From 02f79faf8474e83c7550e5a177806895702c86c7 Mon Sep 17 00:00:00 2001 From: Kasper Lund Date: Fri, 3 Feb 2023 08:54:39 +0100 Subject: [PATCH] Support multiple providers for the same service (#1393) * Work in progress * More work * Better comments * Refactor * Update gold * Add service priorities * Allow multiple handlers per service provider * Start extending new test case * Implement hacky disambiguation * Update gold * Add disambiguation filter * More filtering options * Introduce service selectors * Minor cleanups * Apply suggestions from code review Co-authored-by: Florian Loitsch * Introduce more selectors * Use WifiService.SELECTOR * Allow unknown root cert for wong.host.badssl.com * Cleanups * Improve service network test using tags * Fix health --------- Co-authored-by: Florian Loitsch --- examples/service.toit | 26 +- lib/core/message_.toit | 10 +- lib/core/print.toit | 4 +- lib/log/target.toit | 4 +- lib/net/cellular.toit | 3 +- lib/net/impl.toit | 3 +- lib/net/wifi.toit | 3 +- lib/system/api/cellular.toit | 19 +- lib/system/api/containers.toit | 18 +- lib/system/api/firmware.toit | 18 +- lib/system/api/log.toit | 20 +- lib/system/api/network.toit | 54 +- lib/system/api/print.toit | 20 +- lib/system/api/service_discovery.toit | 54 +- lib/system/api/trace.toit | 21 +- lib/system/api/wifi.toit | 17 +- lib/system/base/firmware.toit | 7 +- lib/system/base/network.toit | 16 +- lib/system/containers.toit | 3 +- lib/system/firmware.toit | 3 +- lib/system/services.toit | 499 +++++++++++++----- lib/system/trace.toit | 2 +- system/containers.toit | 14 +- system/extensions/esp32/boot.toit | 4 +- system/extensions/esp32/firmware.toit | 10 +- system/extensions/esp32/wifi.toit | 8 +- system/extensions/host/initialize.toit | 2 +- system/extensions/host/network.toit | 8 +- system/extensions/shared/network_base.toit | 7 +- system/flash/image_writer.toit | 6 +- system/initialize.toit | 4 +- system/services.toit | 150 ++++-- tests/firmware_map_test.toit | 6 +- tests/log_test.toit | 2 +- .../gold/illegal_system_call_test.gold | 6 +- tests/negative/gold/invalid_program_test.gold | 2 +- tests/negative/illegal_system_call_test.toit | 1 + tests/negative/services_trace_fail_test.toit | 2 +- .../services_trace_unhandled_test.toit | 2 +- .../services_trace_uninstalled_test.toit | 2 +- tests/process_test_slow.toit | 30 +- tests/services_extension_test.toit | 6 +- ...e_log_test.toit => services_log_test.toit} | 2 +- tests/services_multi_test.toit | 289 ++++++++++ tests/services_network_test.toit | 37 +- tests/services_notify_test.toit | 4 +- tests/services_print_test.toit | 2 +- tests/services_resource_test.toit | 6 +- tests/services_simple_test.toit | 2 +- tests/services_trace_test.toit | 2 +- tests/tls_test_slow.toit | 2 +- 51 files changed, 1036 insertions(+), 406 deletions(-) rename tests/{service_log_test.toit => services_log_test.toit} (97%) create mode 100644 tests/services_multi_test.toit diff --git a/examples/service.toit b/examples/service.toit index 0949110d6..05fb48c1e 100644 --- a/examples/service.toit +++ b/examples/service.toit @@ -12,11 +12,12 @@ import system.services main: spawn:: - service := LogServiceDefinition + service := LogServiceProvider service.install service.uninstall --wait // Wait until last client closes. logger := LogServiceClient + logger.open logger.log "Hello" logger.log "World" logger.close @@ -24,31 +25,32 @@ main: // ------------------------------------------------------------------ interface LogService: - static UUID/string ::= "00e1aca5-4861-4ec6-86e6-eea82936af13" - static MAJOR/int ::= 1 - static MINOR/int ::= 0 + static SELECTOR ::= services.ServiceSelector + --uuid="00e1aca5-4861-4ec6-86e6-eea82936af13" + --major=1 + --minor=0 - static LOG_INDEX ::= 0 log message/string -> none + static LOG_INDEX ::= 0 // ------------------------------------------------------------------ class LogServiceClient extends services.ServiceClient implements LogService: - constructor --open/bool=true: - super --open=open - - open -> LogServiceClient?: - return (open_ LogService.UUID LogService.MAJOR LogService.MINOR) and this + static SELECTOR ::= LogService.SELECTOR + constructor selector/services.ServiceSelector=SELECTOR: + assert: selector.matches SELECTOR + super selector log message/string -> none: invoke_ LogService.LOG_INDEX message // ------------------------------------------------------------------ -class LogServiceDefinition extends services.ServiceDefinition implements LogService: +class LogServiceProvider extends services.ServiceProvider + implements LogService services.ServiceHandler: constructor: super "log" --major=1 --minor=0 - provides LogService.UUID LogService.MAJOR LogService.MINOR + provides LogService.SELECTOR --handler=this handle pid/int client/int index/int arguments/any -> any: if index == LogService.LOG_INDEX: return log arguments diff --git a/lib/core/message_.toit b/lib/core/message_.toit index 5c28ed69f..3c352f78e 100644 --- a/lib/core/message_.toit +++ b/lib/core/message_.toit @@ -8,11 +8,11 @@ SYSTEM_SPAWNED_ ::= 1 SYSTEM_TRACE_ ::= 2 // Stack traces, histograms, and profiling information. // System message types for service RPCs. -SYSTEM_RPC_REQUEST_ ::= 3 -SYSTEM_RPC_REPLY_ ::= 4 -SYSTEM_RPC_CANCEL_ ::= 5 -SYSTEM_RPC_NOTIFY_ ::= 6 -SYSTEM_RPC_NOTIFY_RESOURCE_ ::= 7 +SYSTEM_RPC_REQUEST_ ::= 3 +SYSTEM_RPC_REPLY_ ::= 4 +SYSTEM_RPC_CANCEL_ ::= 5 +SYSTEM_RPC_NOTIFY_TERMINATED_ ::= 6 +SYSTEM_RPC_NOTIFY_RESOURCE_ ::= 7 /** Sends the $message with $type to the process identified by $pid. diff --git a/lib/core/print.toit b/lib/core/print.toit index 480a2a3a7..76b3221c9 100644 --- a/lib/core/print.toit +++ b/lib/core/print.toit @@ -83,8 +83,8 @@ write_on_stderr_ message/string add_newline/bool -> none: /** Print service used by $print. */ -service_/PrintService ::= (PrintServiceClient --no-open).open or - StandardPrintService_ +service_/PrintService ::= (PrintServiceClient).open + --if_absent=: StandardPrintService_ /** Standard print service used when the system print service cannot diff --git a/lib/log/target.toit b/lib/log/target.toit index 74c70ffae..c3bb8205d 100644 --- a/lib/log/target.toit +++ b/lib/log/target.toit @@ -17,8 +17,8 @@ class DefaultTarget implements Target: /** Log service used by $DefaultTarget. */ -service_/LogService ::= (LogServiceClient --no-open).open or - StandardLogService_ +service_/LogService ::= (LogServiceClient).open + --if_absent=: StandardLogService_ /** Standard log service used when the system log service cannot diff --git a/lib/net/cellular.toit b/lib/net/cellular.toit index 91fcb6c63..71af5f944 100644 --- a/lib/net/cellular.toit +++ b/lib/net/cellular.toit @@ -30,7 +30,8 @@ CONFIG_OPEN_DRAIN /int ::= 2 CONFIG_PRIORITY_LOW /int ::= 0 CONFIG_PRIORITY_HIGH /int ::= 1 -service_/CellularServiceClient? ::= (CellularServiceClient --no-open).open +service_/CellularServiceClient? ::= (CellularServiceClient).open + --if_absent=: null open config/Map? -> net.Interface: service := service_ diff --git a/lib/net/impl.toit b/lib/net/impl.toit index 101ff1f3d..25289bfbf 100644 --- a/lib/net/impl.toit +++ b/lib/net/impl.toit @@ -14,7 +14,8 @@ import .modules.udp import system.api.network show NetworkService NetworkServiceClient import system.base.network show NetworkResourceProxy -service_/NetworkServiceClient? ::= (NetworkServiceClient --no-open).open +service_/NetworkServiceClient? ::= (NetworkServiceClient).open + --if_absent=: null open -> net.Interface: service := service_ diff --git a/lib/net/wifi.toit b/lib/net/wifi.toit index c937e68f6..1fc2752b3 100644 --- a/lib/net/wifi.toit +++ b/lib/net/wifi.toit @@ -32,7 +32,8 @@ WIFI_SCAN_ELEMENT_COUNT_ ::= 5 SCAN_TIMEOUT_MS_/int := 1000 -service_/WifiServiceClient? ::= (WifiServiceClient --no-open).open +service_/WifiServiceClient? ::= (WifiServiceClient).open + --if_absent=: null class AccessPoint: ssid/string diff --git a/lib/system/api/cellular.toit b/lib/system/api/cellular.toit index b50acf327..a1bbf21e5 100644 --- a/lib/system/api/cellular.toit +++ b/lib/system/api/cellular.toit @@ -3,21 +3,22 @@ // found in the lib/LICENSE file. import system.api.network show NetworkService NetworkServiceClient +import system.services show ServiceSelector interface CellularService extends NetworkService: - static UUID /string ::= "83798564-d965-49bf-b69d-7f05a082f4f0" - static MAJOR /int ::= 0 - static MINOR /int ::= 2 + static SELECTOR ::= ServiceSelector + --uuid="83798564-d965-49bf-b69d-7f05a082f4f0" + --major=0 + --minor=2 - static CONNECT_INDEX /int ::= 1000 connect config/Map? -> List + static CONNECT_INDEX /int ::= 1000 class CellularServiceClient extends NetworkServiceClient implements CellularService: - constructor --open/bool=true: - super --open=open - - open -> CellularServiceClient?: - return (open_ CellularService.UUID CellularService.MAJOR CellularService.MINOR) and this + static SELECTOR ::= CellularService.SELECTOR + constructor selector/ServiceSelector=SELECTOR: + assert: selector.matches SELECTOR + super selector connect config/Map? -> List: return invoke_ CellularService.CONNECT_INDEX config diff --git a/lib/system/api/containers.toit b/lib/system/api/containers.toit index 0c6618ee7..bf993b7a9 100644 --- a/lib/system/api/containers.toit +++ b/lib/system/api/containers.toit @@ -3,13 +3,14 @@ // found in the lib/LICENSE file. import uuid -import system.services show ServiceClient +import system.services show ServiceSelector ServiceClient import system.containers show ContainerImage interface ContainerService: - static UUID /string ::= "358ee529-45a4-409e-8fab-7a28f71e5c51" - static MAJOR /int ::= 0 - static MINOR /int ::= 6 + static SELECTOR ::= ServiceSelector + --uuid="358ee529-45a4-409e-8fab-7a28f71e5c51" + --major=0 + --minor=6 static FLAG_RUN_BOOT /int ::= 1 << 0 static FLAG_RUN_CRITICAL /int ::= 1 << 1 @@ -39,11 +40,10 @@ interface ContainerService: static IMAGE_WRITER_COMMIT_INDEX /int ::= 5 class ContainerServiceClient extends ServiceClient implements ContainerService: - constructor --open/bool=true: - super --open=open - - open -> ContainerServiceClient?: - return (open_ ContainerService.UUID ContainerService.MAJOR ContainerService.MINOR) and this + static SELECTOR ::= ContainerService.SELECTOR + constructor selector/ServiceSelector=SELECTOR: + assert: selector.matches SELECTOR + super selector list_images -> List: array := invoke_ ContainerService.LIST_IMAGES_INDEX null diff --git a/lib/system/api/firmware.toit b/lib/system/api/firmware.toit index 152f76dd2..86df5ce75 100644 --- a/lib/system/api/firmware.toit +++ b/lib/system/api/firmware.toit @@ -2,12 +2,13 @@ // Use of this source code is governed by an MIT-style license that can be // found in the lib/LICENSE file. -import system.services show ServiceClient +import system.services show ServiceSelector ServiceClient interface FirmwareService: - static UUID /string ::= "777096e8-05bc-4af7-919e-5ba696549bd5" - static MAJOR /int ::= 0 - static MINOR /int ::= 5 + static SELECTOR ::= ServiceSelector + --uuid="777096e8-05bc-4af7-919e-5ba696549bd5" + --major=0 + --minor=5 is_validation_pending -> bool static IS_VALIDATION_PENDING_INDEX /int ::= 0 @@ -49,11 +50,10 @@ interface FirmwareService: static FIRMWARE_WRITER_COMMIT_INDEX /int ::= 7 class FirmwareServiceClient extends ServiceClient implements FirmwareService: - constructor --open/bool=true: - super --open=open - - open -> FirmwareServiceClient?: - return (open_ FirmwareService.UUID FirmwareService.MAJOR FirmwareService.MINOR) and this + static SELECTOR ::= FirmwareService.SELECTOR + constructor selector/ServiceSelector=SELECTOR: + assert: selector.matches SELECTOR + super selector is_validation_pending -> bool: return invoke_ FirmwareService.IS_VALIDATION_PENDING_INDEX null diff --git a/lib/system/api/log.toit b/lib/system/api/log.toit index 284f20c14..033b312d1 100644 --- a/lib/system/api/log.toit +++ b/lib/system/api/log.toit @@ -2,22 +2,22 @@ // Use of this source code is governed by an MIT-style license that can be // found in the lib/LICENSE file. -import system.services show ServiceClient +import system.services show ServiceSelector ServiceClient interface LogService: - static UUID /string ::= "89e6340c-67f5-4055-b1d1-b4f4c2755f67" - static MAJOR /int ::= 0 - static MINOR /int ::= 1 + static SELECTOR ::= ServiceSelector + --uuid="89e6340c-67f5-4055-b1d1-b4f4c2755f67" + --major=0 + --minor=1 - static LOG_INDEX /int ::= 0 log level/int message/string names/List? keys/List? values/List? -> none + static LOG_INDEX /int ::= 0 class LogServiceClient extends ServiceClient implements LogService: - constructor --open/bool=true: - super --open=open - - open -> LogServiceClient?: - return (open_ LogService.UUID LogService.MAJOR LogService.MINOR) and this + static SELECTOR ::= LogService.SELECTOR + constructor selector/ServiceSelector=SELECTOR: + assert: selector.matches SELECTOR + super selector log level/int message/string names/List? keys/List? values/List? -> none: invoke_ LogService.LOG_INDEX [level, message, names, keys, values] diff --git a/lib/system/api/network.toit b/lib/system/api/network.toit index 872133160..4a4479586 100644 --- a/lib/system/api/network.toit +++ b/lib/system/api/network.toit @@ -6,15 +6,16 @@ import net import net.udp import net.tcp -import system.services show ServiceClient +import system.services show ServiceSelector ServiceClient // For references in documentation comments. import system.services show ServiceResource ServiceResourceProxy interface NetworkService: - static UUID /string ::= "063e228a-3a7a-44a8-b024-d55127255ccb" - static MAJOR /int ::= 0 - static MINOR /int ::= 3 + static SELECTOR ::= ServiceSelector + --uuid="063e228a-3a7a-44a8-b024-d55127255ccb" + --major=0 + --minor=3 /** Proxy mask bits that indicate which operations must be proxied @@ -43,66 +44,65 @@ interface NetworkService: // the proxy mask bits in a list. The proxy mask bits indicate // which operations the service definition wants the client to // proxy through it. - static CONNECT_INDEX /int ::= 0 connect -> List + static CONNECT_INDEX /int ::= 0 - static ADDRESS_INDEX /int ::= 1 address handle/int -> ByteArray + static ADDRESS_INDEX /int ::= 1 - static RESOLVE_INDEX /int ::= 2 resolve handle/int host/string -> List + static RESOLVE_INDEX /int ::= 2 - static UDP_OPEN_INDEX /int ::= 100 udp_open handle/int port/int? -> int + static UDP_OPEN_INDEX /int ::= 100 - static UDP_CONNECT_INDEX /int ::= 101 udp_connect handle/int ip/ByteArray port/int -> none + static UDP_CONNECT_INDEX /int ::= 101 - static UDP_RECEIVE_INDEX /int ::= 102 udp_receive handle/int -> List + static UDP_RECEIVE_INDEX /int ::= 102 - static UDP_SEND_INDEX /int ::= 103 udp_send handle/int data/ByteArray ip/ByteArray port/int -> none + static UDP_SEND_INDEX /int ::= 103 - static TCP_CONNECT_INDEX /int ::= 200 tcp_connect handle/int ip/ByteArray port/int -> int + static TCP_CONNECT_INDEX /int ::= 200 - static TCP_LISTEN_INDEX /int ::= 201 tcp_listen handle/int port/int -> int + static TCP_LISTEN_INDEX /int ::= 201 - static TCP_ACCEPT_INDEX /int ::= 202 tcp_accept handle/int -> int + static TCP_ACCEPT_INDEX /int ::= 202 - static TCP_CLOSE_WRITE_INDEX /int ::= 203 tcp_close_write handle/int -> none + static TCP_CLOSE_WRITE_INDEX /int ::= 203 - static SOCKET_GET_OPTION_INDEX /int ::= 300 socket_get_option handle/int option/int -> any + static SOCKET_GET_OPTION_INDEX /int ::= 300 - static SOCKET_SET_OPTION_INDEX /int ::= 301 socket_set_option handle/int option/int value/any -> none + static SOCKET_SET_OPTION_INDEX /int ::= 301 - static SOCKET_LOCAL_ADDRESS_INDEX /int ::= 302 socket_local_address handle/int -> List + static SOCKET_LOCAL_ADDRESS_INDEX /int ::= 302 - static SOCKET_PEER_ADDRESS_INDEX /int ::= 303 socket_peer_address handle/int -> List + static SOCKET_PEER_ADDRESS_INDEX /int ::= 303 - static SOCKET_READ_INDEX /int ::= 304 socket_read handle/int -> ByteArray? + static SOCKET_READ_INDEX /int ::= 304 - static SOCKET_WRITE_INDEX /int ::= 305 socket_write handle/int data -> int + static SOCKET_WRITE_INDEX /int ::= 305 - static SOCKET_MTU_INDEX /int ::= 306 socket_mtu handle/int -> int + static SOCKET_MTU_INDEX /int ::= 306 class NetworkServiceClient extends ServiceClient implements NetworkService: - constructor --open/bool=true: - super --open=open - - open -> NetworkServiceClient?: - return (open_ NetworkService.UUID NetworkService.MAJOR NetworkService.MINOR) and this + static SELECTOR ::= NetworkService.SELECTOR + constructor selector/ServiceSelector=SELECTOR: + assert: selector.matches SELECTOR + super selector connect -> List: return invoke_ NetworkService.CONNECT_INDEX null diff --git a/lib/system/api/print.toit b/lib/system/api/print.toit index d8ef625b3..5fbae5dfa 100644 --- a/lib/system/api/print.toit +++ b/lib/system/api/print.toit @@ -2,22 +2,22 @@ // Use of this source code is governed by an MIT-style license that can be // found in the lib/LICENSE file. -import system.services show ServiceClient +import system.services show ServiceSelector ServiceClient interface PrintService: - static UUID /string ::= "0b7e3aa1-9fc9-4632-bb09-4605cd11897e" - static MAJOR /int ::= 0 - static MINOR /int ::= 1 + static SELECTOR ::= ServiceSelector + --uuid="0b7e3aa1-9fc9-4632-bb09-4605cd11897e" + --major=0 + --minor=1 - static PRINT_INDEX /int ::= 0 print message/string -> none + static PRINT_INDEX /int ::= 0 class PrintServiceClient extends ServiceClient implements PrintService: - constructor --open/bool=true: - super --open=open - - open -> PrintServiceClient?: - return (open_ PrintService.UUID PrintService.MAJOR PrintService.MINOR) and this + static SELECTOR ::= PrintService.SELECTOR + constructor selector/ServiceSelector=SELECTOR: + assert: selector.matches SELECTOR + super selector print message/string -> none: invoke_ PrintService.PRINT_INDEX message diff --git a/lib/system/api/service_discovery.toit b/lib/system/api/service_discovery.toit index 8d55dfbf1..3e409f292 100644 --- a/lib/system/api/service_discovery.toit +++ b/lib/system/api/service_discovery.toit @@ -3,34 +3,56 @@ // Use of this source code is governed by an MIT-style license that can be // found in the lib/LICENSE file. -import system.services show ServiceClient +import system.services show ServiceClient ServiceSelector interface ServiceDiscoveryService: - static UUID /string ::= "dc58d7e1-1b1f-4a93-a9ac-bd45a47d7de8" - static MAJOR /int ::= 0 - static MINOR /int ::= 2 + static SELECTOR ::= ServiceSelector + --uuid="dc58d7e1-1b1f-4a93-a9ac-bd45a47d7de8" + --major=0 + --minor=3 + discover uuid/string --wait/bool -> List? static DISCOVER_INDEX /int ::= 0 - discover uuid/string wait/bool -> int? + watch pid/int -> none + static WATCH_INDEX /int ::= 3 + + listen id/int uuid/string -> none + --name/string + --major/int + --minor/int + --priority/int + --tags/List? static LISTEN_INDEX /int ::= 1 - listen uuid/string -> none + unlisten id/int -> none static UNLISTEN_INDEX /int ::= 2 - unlisten uuid/string -> none class ServiceDiscoveryServiceClient extends ServiceClient implements ServiceDiscoveryService: - constructor --open/bool=true: - super --open=open + static SELECTOR ::= ServiceDiscoveryService.SELECTOR + constructor selector/ServiceSelector=SELECTOR: + assert: selector.matches SELECTOR + super selector open -> ServiceDiscoveryServiceClient?: - return (open_ ServiceDiscoveryService.UUID ServiceDiscoveryService.MAJOR ServiceDiscoveryService.MINOR --pid=-1) and this + client := _open_ selector --pid=-1 --id=0 // Hardcoded in system process. + return client and this - discover uuid/string wait/bool -> int?: + discover uuid/string --wait/bool -> List?: return invoke_ ServiceDiscoveryService.DISCOVER_INDEX [uuid, wait] - listen uuid/string -> none: - invoke_ ServiceDiscoveryService.LISTEN_INDEX uuid - - unlisten uuid/string -> none: - invoke_ ServiceDiscoveryService.UNLISTEN_INDEX uuid + watch pid/int -> none: + invoke_ ServiceDiscoveryService.WATCH_INDEX pid + + listen id/int uuid/string -> none + --name/string + --major/int + --minor/int + --priority/int + --tags/List?: + invoke_ ServiceDiscoveryService.LISTEN_INDEX [ + id, uuid, name, major, minor, priority, tags + ] + + unlisten id/int -> none: + invoke_ ServiceDiscoveryService.UNLISTEN_INDEX id diff --git a/lib/system/api/trace.toit b/lib/system/api/trace.toit index 60cb7a635..b9f0ad3b3 100644 --- a/lib/system/api/trace.toit +++ b/lib/system/api/trace.toit @@ -2,12 +2,13 @@ // Use of this source code is governed by an MIT-style license that can be // found in the lib/LICENSE file. -import system.services show ServiceClient +import system.services show ServiceSelector ServiceClient interface TraceService: - static UUID /string ::= "41c6019e-ca48-4847-9673-0869355da76a" - static MAJOR /int ::= 0 - static MINOR /int ::= 1 + static SELECTOR ::= ServiceSelector + --uuid="41c6019e-ca48-4847-9673-0869355da76a" + --major=0 + --minor=1 /** Attempts to handle an encoded trace message usually by printing, @@ -18,16 +19,12 @@ interface TraceService: */ handle_trace message/ByteArray -> bool static HANDLE_TRACE_INDEX /int ::= 0 - // TODO(kasper): It seems nice to always have the method index - // after the method definition to allow for documentation comments. - // This should be fixed across the code base. class TraceServiceClient extends ServiceClient implements TraceService: - constructor --open/bool=true: - super --open=open - - open -> TraceServiceClient?: - return (open_ TraceService.UUID TraceService.MAJOR TraceService.MINOR) and this + static SELECTOR ::= TraceService.SELECTOR + constructor selector/ServiceSelector=SELECTOR: + assert: selector.matches SELECTOR + super selector handle_trace message/ByteArray -> bool: return invoke_ TraceService.HANDLE_TRACE_INDEX message diff --git a/lib/system/api/wifi.toit b/lib/system/api/wifi.toit index d86623afe..9a997c45e 100644 --- a/lib/system/api/wifi.toit +++ b/lib/system/api/wifi.toit @@ -3,11 +3,13 @@ // found in the lib/LICENSE file. import system.api.network show NetworkService NetworkServiceClient +import system.services show ServiceSelector interface WifiService extends NetworkService: - static UUID /string ::= "2436edc6-4cd8-4834-8ebc-ed883990da40" - static MAJOR /int ::= 0 - static MINOR /int ::= 9 + static SELECTOR ::= ServiceSelector + --uuid="2436edc6-4cd8-4834-8ebc-ed883990da40" + --major=0 + --minor=9 connect config/Map? -> List static CONNECT_INDEX /int ::= 1000 @@ -25,11 +27,10 @@ interface WifiService extends NetworkService: static CONFIGURE_INDEX /int ::= 1004 class WifiServiceClient extends NetworkServiceClient implements WifiService: - constructor --open/bool=true: - super --open=open - - open -> WifiServiceClient?: - return (open_ WifiService.UUID WifiService.MAJOR WifiService.MINOR) and this + static SELECTOR ::= WifiService.SELECTOR + constructor selector/ServiceSelector=SELECTOR: + assert: selector.matches SELECTOR + super selector connect config/Map? -> List: return invoke_ WifiService.CONNECT_INDEX config diff --git a/lib/system/base/firmware.toit b/lib/system/base/firmware.toit index f7b755f61..06fb886b4 100644 --- a/lib/system/base/firmware.toit +++ b/lib/system/base/firmware.toit @@ -14,12 +14,13 @@ // directory of this repository. import system.api.firmware show FirmwareService -import system.services show ServiceDefinition ServiceResource +import system.services show ServiceHandler ServiceProvider ServiceResource -abstract class FirmwareServiceDefinitionBase extends ServiceDefinition implements FirmwareService: +abstract class FirmwareServiceProviderBase extends ServiceProvider + implements FirmwareService ServiceHandler: constructor name/string --major/int --minor/int: super name --major=major --minor=minor - provides FirmwareService.UUID FirmwareService.MAJOR FirmwareService.MINOR + provides FirmwareService.SELECTOR --handler=this handle pid/int client/int index/int arguments/any -> any: if index == FirmwareService.IS_VALIDATION_PENDING_INDEX: diff --git a/lib/system/base/network.toit b/lib/system/base/network.toit index 3cf27a72c..5819d6a0c 100644 --- a/lib/system/base/network.toit +++ b/lib/system/base/network.toit @@ -14,7 +14,8 @@ import system.api.network import system.services show ServiceClient - ServiceDefinition + ServiceHandler + ServiceProvider ServiceResource ServiceResourceProxy @@ -53,8 +54,8 @@ interface NetworkModule: class NetworkResource extends ServiceResource: state_/NetworkState ::= ? - constructor service/ServiceDefinition client/int .state_ --notifiable/bool=false: - super service client --notifiable=notifiable + constructor provider/ServiceProvider client/int .state_ --notifiable/bool=false: + super provider client --notifiable=notifiable on_closed -> none: critical_do: state_.down @@ -104,12 +105,13 @@ monitor NetworkState: // ---------------------------------------------------------------------------- /** -The $ProxyingNetworkServiceDefinition makes it easy to proxy a network +The $ProxyingNetworkServiceProvider makes it easy to proxy a network interface and expose it as a provided service. The service can then be used across process boundaries, which makes it possible to run network drivers separate from the rest of the system. */ -abstract class ProxyingNetworkServiceDefinition extends ServiceDefinition implements NetworkModule: +abstract class ProxyingNetworkServiceProvider extends ServiceProvider + implements NetworkModule ServiceHandler: state_/NetworkState ::= NetworkState network_/net.Interface? := null @@ -338,7 +340,7 @@ class TcpServerSocketResourceProxy_ extends ServiceResourceProxy implements tcp. class ProxyingSocketResource_ extends ServiceResource: socket/any ::= ? - constructor service/ServiceDefinition client/int .socket: - super service client + constructor provider/ServiceProvider client/int .socket: + super provider client on_closed -> none: socket.close diff --git a/lib/system/containers.toit b/lib/system/containers.toit index 25f5aaf4d..5a933df3b 100644 --- a/lib/system/containers.toit +++ b/lib/system/containers.toit @@ -13,7 +13,8 @@ import monitor import system.api.containers show ContainerService ContainerServiceClient import system.services show ServiceResourceProxy -_client_ /ContainerServiceClient ::= ContainerServiceClient +_client_ /ContainerServiceClient ::= + (ContainerServiceClient).open as ContainerServiceClient images -> List: return _client_.list_images diff --git a/lib/system/firmware.toit b/lib/system/firmware.toit index dbba63e2e..587bf6ca4 100644 --- a/lib/system/firmware.toit +++ b/lib/system/firmware.toit @@ -9,7 +9,8 @@ User-space side of the RPC API for updating the firmware. import system.api.firmware show FirmwareServiceClient import system.services show ServiceResourceProxy -_client_ /FirmwareServiceClient? ::= (FirmwareServiceClient --no-open).open +_client_ /FirmwareServiceClient? ::= (FirmwareServiceClient).open + --if_absent=: null /** The configuration of the current firmware. diff --git a/lib/system/services.toit b/lib/system/services.toit index 859a010db..53998e354 100644 --- a/lib/system/services.toit +++ b/lib/system/services.toit @@ -15,10 +15,6 @@ import system.api.service_discovery ServiceDiscoveryService ServiceDiscoveryServiceClient -// Notification kinds. -SERVICES_MANAGER_NOTIFY_ADD_PROCESS /int ::= 0 -SERVICES_MANAGER_NOTIFY_REMOVE_PROCESS /int ::= 1 - // RPC procedure numbers used for using services from clients. RPC_SERVICES_OPEN_ /int ::= 300 RPC_SERVICES_CLOSE_ /int ::= 301 @@ -26,49 +22,229 @@ RPC_SERVICES_INVOKE_ /int ::= 302 RPC_SERVICES_CLOSE_RESOURCE_ /int ::= 303 // Internal limits. -CLIENT_ID_LIMIT_ /int ::= 0x3fff_ffff +RANDOM_ID_LIMIT_ /int ::= 0x3fff_ffff RESOURCE_HANDLE_LIMIT_ /int ::= 0x1fff_ffff // Will be shifted up by one. -_client_ /ServiceDiscoveryService ::= ServiceDiscoveryServiceClient +_client_ /ServiceDiscoveryService ::= (ServiceDiscoveryServiceClient).open + +/** +A service selector is used to identify and discover services. It + has a unique id that never changes and major and minor versions + numbers that support evolving APIs over time. + +On the $ServiceProvider side, the selector is used when providing + a service so that clients can discover it later. + +On the $ServiceClient side, the selector is used when discovering + services, and in this context it can also be restricted to help + disambiguate between multiple variants of a service provided + by multiple providers. +*/ +class ServiceSelector: + uuid/string + major/int + minor/int + constructor --.uuid --.major --.minor: + + /** + Returns a restricted variant of this $ServiceSelector that can + be used in the service discovery process to allow and deny + discovered services. + */ + restrict -> ServiceSelectorRestricted: + return ServiceSelectorRestricted.internal_ this + + /** + Whether this $ServiceSelector matches $selector and thus + identifies the same version of a specific service API. + */ + matches selector/ServiceSelector -> bool: + return uuid == selector.uuid and + major == selector.major and + minor == selector.minor + + is_allowed_ --name/string --major/int --minor/int --tags/List? -> bool: + return true + +class ServiceSelectorRestricted extends ServiceSelector: + tags_ := {:} // Map + names_ ::= {:} // Map> + + tags_include_allowed_/bool := false + names_include_allowed_/bool := false + + constructor.internal_ selector/ServiceSelector: + super --uuid=selector.uuid --major=selector.major --minor=selector.minor + + restrict -> ServiceSelectorRestricted: + throw "Already restricted" + + allow --name/string --major/int?=null --minor/int?=null -> ServiceSelectorRestricted: + return add_name_ --name=name --major=major --minor=minor --allow + deny --name/string --major/int?=null --minor/int?=null -> ServiceSelectorRestricted: + return add_name_ --name=name --major=major --minor=minor --no-allow + + allow --tag/string -> ServiceSelectorRestricted: + return allow --tags=[tag] + allow --tags/List -> ServiceSelectorRestricted: + return add_tags_ --tags=tags --allow + deny --tag/string -> ServiceSelectorRestricted: + return deny --tags=[tag] + deny --tags/List -> ServiceSelectorRestricted: + return add_tags_ --tags=tags --no-allow + + add_name_ --name/string --major/int? --minor/int? --allow/bool -> ServiceSelectorRestricted: + if minor and not major: throw "Must have major version to match on minor" + restrictions := names_.get name --init=: [] + // Check that the new restriction doesn't conflict with an existing one. + restrictions.do: | restriction/ServiceSelectorRestriction_ | + match := true + if major: match = (not restriction.major) or restriction.major == major + if match and minor: match = (not restriction.minor) or restriction.minor == minor + if match: throw "Cannot have multiple entries for the same named version" + if allow: names_include_allowed_ = true + restrictions.add (ServiceSelectorRestriction_ allow major minor) + return this + + add_tags_ --tags/List --allow/bool -> ServiceSelectorRestricted: + tags.do: | tag/string | + if (tags_.get tag) == (not allow): throw "Cannot allow and deny the same tag" + if allow: tags_include_allowed_ = true + tags_[tag] = allow + return this + + is_allowed_ --name/string --major/int --minor/int --tags/List? -> bool: + // Check that the name and versions are allowed. + restrictions := names_.get name + name_allowed := not names_include_allowed_ + if restrictions: restrictions.do: | restriction/ServiceSelectorRestriction_? | + match := (not restriction.major) or restriction.major == major + if match: match = (not restriction.minor) or restriction.minor == minor + if not match: continue.do + if not restriction.allow: return false + // We found named version that was explicitly allowed. Continue through + // the restrictions so we can find any explicitly denied named versions. + name_allowed = true + if not name_allowed: return false + + // Check that the tag is allowed. If no tag is registered as allowed, + // we allow all non-denied tags. + tags_allowed := not tags_include_allowed_ + if tags: tags.do: | tag/string | + tags_.get tag --if_present=: | allowed/bool | + if not allowed: return false + // We found a tag that was explicitly allowed. Continue through + // the tags so we can find any explicitly denied tags. + tags_allowed = true + return tags_allowed + +/** +Base class for clients that connect to and use provided services + (see $ServiceProvider). + +Typically, users call the $open method on a subclass of the client. This then + discovers the corresponding provider and connects to it. + +Subclasses implement service-specific methods to provide convenient APIs. +*/ +class ServiceClient: + // TODO(kasper): Make this non-nullable. + selector/ServiceSelector? -abstract class ServiceClient: _id_/int? := null _pid_/int? := null _name_/string? := null - _version_/List? := null - _default_timeout_/Duration? ::= ? + _major_/int := 0 + _minor_/int := 0 + _patch_/int := 0 + _tags_/List? := null static DEFAULT_OPEN_TIMEOUT /Duration ::= Duration --ms=100 + // TODO(kasper): Deprecate this. + _default_timeout_/Duration? ::= ? + + // TODO(kasper): Deprecate this constructor. constructor --open/bool=true: // If we're opening the client as part of constructing it, we instruct the // service discovery service to wait for the requested service to be provided. + selector = null _default_timeout_ = open ? DEFAULT_OPEN_TIMEOUT : null if open and not this.open: throw "Cannot find service" - abstract open -> ServiceClient? - + // TODO(kasper): Deprecate this helper. open_ uuid/string major/int minor/int -> ServiceClient? - --pid/int?=null --timeout/Duration?=_default_timeout_: - if _id_: throw "Already opened" - if pid: - process_send_ pid SYSTEM_RPC_NOTIFY_ [SERVICES_MANAGER_NOTIFY_ADD_PROCESS, Process.current.id] + assert: not this.selector + return _open_ (ServiceSelector --uuid=uuid --major=major --minor=minor) + --timeout=timeout + + constructor selector/ServiceSelector: + // TODO(kasper): Simplify this once the we don't need the + // legacy constructor. + this.selector = selector + _default_timeout_ = null + + open --timeout/Duration?=DEFAULT_OPEN_TIMEOUT -> ServiceClient: + return open --timeout=timeout --if_absent=: throw "Cannot find service" + + open --timeout/Duration?=null [--if_absent] -> any: + if not selector: throw "Must override open in client" + assert: not this._default_timeout_ + if client := _open_ selector --timeout=timeout: return client + return if_absent.call + + _open_ selector/ServiceSelector --timeout/Duration? -> ServiceClient?: + discovered/List? := null + if timeout: + catch --unwind=(: it != DEADLINE_EXCEEDED_ERROR): + with_timeout timeout: discovered = _client_.discover selector.uuid --wait else: - if timeout: - catch --unwind=(: it != DEADLINE_EXCEEDED_ERROR): - with_timeout timeout: pid = _client_.discover uuid true + discovered = _client_.discover selector.uuid --no-wait + if not discovered: return null + + candidate_index := null + candidate_priority := null + for i := 0; i < discovered.size; i += 7: + tags := discovered[i + 6] + allowed := selector.is_allowed_ + --name=discovered[i + 2] + --major=discovered[i + 3] + --minor=discovered[i + 4] + --tags=tags + if not allowed: continue + priority := discovered[i + 5] + if not candidate_index: + candidate_index = i + candidate_priority = priority + else if priority < candidate_priority: + // The remaining entries have lower priorities and + // we already found a suitable candidate. + break else: - pid = _client_.discover uuid false - if not pid: return null + // Found multiple candidates with the same priority. + throw "Cannot disambiguate" + + if not candidate_index: return null + pid := discovered[candidate_index] + id := discovered[candidate_index + 1] + return _open_ selector --pid=pid --id=id + + _open_ selector/ServiceSelector --pid/int --id/int -> ServiceClient: + if _id_: throw "Already opened" // Open the client by doing a RPC-call to the discovered process. // This returns the client id necessary for invoking service methods. - definition ::= rpc.invoke pid RPC_SERVICES_OPEN_ [uuid, major, minor] + definition ::= rpc.invoke pid RPC_SERVICES_OPEN_ [ + id, selector.uuid, selector.major, selector.minor + ] _pid_ = pid - _id_ = definition[2] - _name_ = definition[0] - _version_ = definition[1] + _id_ = definition[0] + _name_ = definition[1] + _major_ = definition[2] + _minor_ = definition[3] + _patch_ = definition[4] + _tags_ = definition[5] // Close the client if the reference goes away, so the service // process can clean things up. add_finalizer this:: close @@ -81,25 +257,25 @@ abstract class ServiceClient: return _name_ major -> int: - return _version_[0] + return _major_ minor -> int: - return _version_[1] + return _minor_ patch -> int: - return _version_[2] + return _patch_ close -> none: id := _id_ if not id: return pid := _pid_ - _id_ = _name_ = _version_ = _pid_ = null + _id_ = _name_ = _pid_ = null remove_finalizer this ServiceResourceProxyManager_.unregister_all id critical_do: rpc.invoke pid RPC_SERVICES_CLOSE_ id stringify -> string: - return "service:$_name_@$(_version_.join ".")" + return "service:$_name_@$(_major_).$(_minor_).$(_patch_)" invoke_ index/int arguments/any -> any: id := _id_ @@ -115,13 +291,41 @@ abstract class ServiceClient: // close after timing out, it should still work. critical_do: rpc.invoke _pid_ RPC_SERVICES_CLOSE_RESOURCE_ [id, handle] -abstract class ServiceDefinition: - name/string ::= ? - _version_/List ::= ? +/** +A handler for requests from clients. - _uuids_/List ::= [] - _versions_/List ::= [] +A $ServiceProvider may provide multiple services, each of which comes with a + handler. That handler is then called for the corresponding request from the + client. +*/ +interface ServiceHandler: + handle pid/int client/int index/int arguments/any-> any + +/** +A service provider. + +Service providers are classes that expose APIs through remote + procedure calls (RPCs). + +# Inheritance +Typically, subclasses implement the $ServiceHandler interface, and + call the $provides method in their constructor, using 'this' as handler. + +If the subclass implements multiple independent service APIs, it is + useful to split the handling out into multiple implementations of + $ServiceHandler to avoid running into issues with overlapping + method indexes. +*/ +class ServiceProvider: + name/string + major/int + minor/int + patch/int + tags/List? + + _services_/List ::= [] _manager_/ServiceManager_? := null + _ids_/List? := null _clients_/Set ::= {} // Set _clients_closed_/int := 0 @@ -130,33 +334,44 @@ abstract class ServiceDefinition: _resources_/Map ::= {:} // Map> _resource_handle_next_/int := ? - constructor .name --major/int --minor/int --patch/int=0: - _version_ = [major, minor, patch] + constructor .name --.major --.minor --.patch=0 --.tags=null: _resource_handle_next_ = random RESOURCE_HANDLE_LIMIT_ - abstract handle pid/int client/int index/int arguments/any-> any - on_opened client/int -> none: // Override in subclasses. on_closed client/int -> none: // Override in subclasses. - version -> string: - return _version_.join "." - stringify -> string: - return "service:$name@$version" + return "service:$name@$(major).$(minor).$(patch)" - provides uuid/string major/int minor/int -> none: - _uuids_.add uuid - _versions_.add [major, minor] + /** + Registers a handler for the given $selector. + + This function should only be called from subclasses (typically in their constructor). + */ + provides selector/ServiceSelector --handler/ServiceHandler -> none + --id/int?=null + --priority/int=100 + --tags/List?=null: + provider_tags := this.tags + if provider_tags: tags = tags ? (provider_tags + tags) : provider_tags + service := Service_ + --selector=selector + --handler=handler + --id=id + --priority=priority + --tags=tags + _services_.add service install -> none: if _manager_: throw "Already installed" _manager_ = ServiceManager_.instance _clients_closed_ = 0 - _uuids_.do: _manager_.listen it this + // TODO(kasper): Handle the case where one of the calls + // to listen fails. + _ids_ = Array_ _services_.size: _manager_.listen _services_[it] this uninstall --wait/bool=false -> none: if wait: @@ -177,7 +392,7 @@ abstract class ServiceDefinition: _open_ client/int -> List: _clients_.add client catch --trace: on_opened client - return [ name, _version_, client ] + return [ client, name, major, minor, patch, tags ] _close_ client/int -> none: resources ::= _resources_.get client @@ -219,27 +434,49 @@ abstract class ServiceDefinition: _resource_handle_next_ = (next >= RESOURCE_HANDLE_LIMIT_) ? 0 : next return (handle << 1) + (notifiable ? 1 : 0) - _validate_ uuid/string major/int minor/int -> none: - index := _uuids_.index_of uuid - if index < 0: throw "$this does not provide service:$uuid" - version := _versions_[index] - if major != version[0]: + _validate_ uuid/string major/int minor/int -> Service_: + service/Service_? := _lookup_ uuid + if not service: throw "$this does not provide service:$uuid" + if major != service.selector.major: throw "$this does not provide service:$uuid@$(major).x" - if minor > version[1]: + if minor > service.selector.minor: throw "$this does not provide service:$uuid@$(major).$(minor).x" + return service + + _lookup_ uuid/string -> Service_?: + _services_.do: | service/Service_ | + if service.selector.uuid == uuid: return service + return null _uninstall_ -> none: if not _resources_.is_empty: throw "Leaked $_resources_" - _uuids_.do: _manager_.unlisten it + // TODO(kasper): Handle the case where one of the calls + // to unlisten fails. + _ids_.do: _manager_.unlisten it + _ids_ = null _manager_ = null +// TODO(kasper): Deprecate this. +abstract class ServiceDefinition extends ServiceProvider implements ServiceHandler: + constructor name/string --major/int --minor/int --patch/int=0: + super name --major=major --minor=minor --patch=patch + + abstract handle pid/int client/int index/int arguments/any -> any + + provides selector/ServiceSelector -> none: + super selector --handler=this + + provides uuid/string major/int minor/int -> none: + selector := ServiceSelector --uuid=uuid --major=major --minor=minor + super selector --handler=this + abstract class ServiceResource implements rpc.RpcSerializable: - _service_/ServiceDefinition? := ? + _provider_/ServiceProvider? := ? _client_/int ::= ? _handle_/int? := null - constructor ._service_ ._client_ --notifiable/bool=false: - _handle_ = _service_._register_resource_ _client_ this notifiable + constructor ._provider_ ._client_ --notifiable/bool=false: + _handle_ = _provider_._register_resource_ _client_ this notifiable abstract on_closed -> none @@ -255,14 +492,14 @@ abstract class ServiceResource implements rpc.RpcSerializable: handle := _handle_ if not handle: throw "ALREADY_CLOSED" if handle & 1 == 0: throw "Resource not notifiable" - _service_._manager_.notify _client_ handle notification + _provider_._manager_.notify _client_ handle notification close -> none: handle := _handle_ if not handle: return - service := _service_ - _handle_ = _service_ = null - service._unregister_resource_ _client_ handle + provider := _provider_ + _handle_ = _provider_ = null + provider._unregister_resource_ _client_ handle on_closed serialize_for_rpc -> int: @@ -299,6 +536,20 @@ abstract class ServiceResourceProxy: ServiceResourceProxyManager_.instance.unregister client_.id handle catch --trace: client_._close_resource_ handle +class Service_: + selector/ServiceSelector + handler/ServiceHandler + id/int? + priority/int + tags/List? + constructor --.selector --.handler --.id --.priority --.tags: + +class ServiceSelectorRestriction_: + allow/bool + major/int? + minor/int? + constructor .allow .major .minor: + class ServiceResourceProxyManager_ implements SystemMessageHandler_: static instance ::= ServiceResourceProxyManager_ static proxies_/Map? := null @@ -337,52 +588,68 @@ class ServiceManager_ implements SystemMessageHandler_: static instance := ServiceManager_ static uninitialized/bool := true - broker_/ServiceRpcBroker_ ::= ServiceRpcBroker_ + broker_/broker.RpcBroker ::= broker.RpcBroker - clients_/Map ::= {:} // Map - clients_by_pid_/Map ::= {:} // Map> + clients_/Map ::= {:} // Map + clients_by_pid_/Map ::= {:} // Map> - services_by_uuid_/Map ::= {:} // Map - services_by_client_/Map ::= {:} // Map + providers_/Map ::= {:} // Map + providers_by_client_/Map ::= {:} // Map + handlers_by_client_/Map ::= {:} // Map constructor: - set_system_message_handler_ SYSTEM_RPC_NOTIFY_ this + set_system_message_handler_ SYSTEM_RPC_NOTIFY_TERMINATED_ this broker_.register_procedure RPC_SERVICES_OPEN_:: | arguments _ pid | - open pid arguments[0] arguments[1] arguments[2] + open pid arguments[0] arguments[1] arguments[2] arguments[3] broker_.register_procedure RPC_SERVICES_CLOSE_:: | arguments | close arguments broker_.register_procedure RPC_SERVICES_INVOKE_:: | arguments _ pid | client/int ::= arguments[0] - service ::= services_by_client_[client] - service.handle pid client arguments[1] arguments[2] + handler/ServiceHandler ::= handlers_by_client_[client] + handler.handle pid client arguments[1] arguments[2] broker_.register_procedure RPC_SERVICES_CLOSE_RESOURCE_:: | arguments | client/int ::= arguments[0] - services_by_client_.get client --if_present=: | service/ServiceDefinition | - resource/ServiceResource? := service._find_resource_ client arguments[1] + providers_by_client_.get client --if_present=: | provider/ServiceProvider | + resource/ServiceResource? := provider._find_resource_ client arguments[1] if resource: resource.close broker_.install uninitialized = false static is_empty -> bool: - return uninitialized or instance.services_by_uuid_.is_empty - - listen uuid/string service/ServiceDefinition -> none: - services_by_uuid_[uuid] = service - _client_.listen uuid - - unlisten uuid/string -> none: - _client_.unlisten uuid - services_by_uuid_.remove uuid - - open pid/int uuid/string major/int minor/int -> List: - service/ServiceDefinition? ::= services_by_uuid_.get uuid - if not service: throw "Unknown service:$uuid" - service._validate_ uuid major minor - client ::= assign_client_id_ pid - services_by_client_[client] = service + return uninitialized or instance.providers_.is_empty + + listen service/Service_ provider/ServiceProvider -> int: + id := assign_id_ service.id providers_ provider + // TODO(kasper): Clean up in the services + // table if listen fails? + _client_.listen id service.selector.uuid + --name=provider.name + --major=provider.major + --minor=provider.minor + --priority=service.priority + --tags=service.tags + return id + + unlisten id/int -> none: + _client_.unlisten id + providers_.remove id + + open pid/int id/int uuid/string major/int minor/int -> List: + provider/ServiceProvider? ::= providers_.get id + if not provider: throw "Unknown service:$id" + service := provider._validate_ uuid major minor + clients/Set ::= clients_by_pid_.get pid --init=(: {}) + if clients.is_empty and pid != Process.current.id: + // From this point forward, we need to be told if the client + // process goes away so we can clean up. + _client_.watch pid + + client ::= assign_id_ null clients_ pid clients.add client - return service._open_ client + providers_by_client_[client] = provider + handlers_by_client_[client] = service.handler + return provider._open_ client notify client/int handle/int notification/any -> none: pid/int? := clients_.get client @@ -394,9 +661,10 @@ class ServiceManager_ implements SystemMessageHandler_: pid/int? := clients_.get client if not pid: return // Already closed. clients_.remove client - // Unregister the client in the service client set. - service/ServiceDefinition := services_by_client_[client] - services_by_client_.remove client + // Unregister the client in the client sets. + provider/ServiceProvider := providers_by_client_[client] + providers_by_client_.remove client + handlers_by_client_.remove client // Only unregister the client from the clients set // for the pid if we haven't already done so as part // of a call to $close_all. @@ -404,7 +672,7 @@ class ServiceManager_ implements SystemMessageHandler_: if clients: clients.remove client if clients.is_empty: clients_by_pid_.remove pid - service._close_ client + provider._close_ client close_all pid/int -> none: clients/Set? ::= clients_by_pid_.get pid @@ -415,36 +683,23 @@ class ServiceManager_ implements SystemMessageHandler_: clients.do: close it on_message type/int gid/int pid/int message/any -> none: - assert: type == SYSTEM_RPC_NOTIFY_ - kind/int ::= message[0] + assert: type == SYSTEM_RPC_NOTIFY_TERMINATED_ // The other process isn't necessarily the sender of the // notifications. They almost always come from the system // process and are sent as part of the discovery handshake. - other/int ::= message[1] - if kind == SERVICES_MANAGER_NOTIFY_ADD_PROCESS: - broker_.add_process other - else if kind == SERVICES_MANAGER_NOTIFY_REMOVE_PROCESS: - broker_.remove_process other - close_all other - else: - unreachable - - assign_client_id_ pid/int -> int: + other/int ::= message + broker_.cancel_requests other + close_all other + + assign_id_ id/int? map/Map value/any -> int: + if not id: + id = random_id_ map + else if map.contains id: + throw "Already registered" + map[id] = value + return id + + random_id_ map/Map -> int: while true: - guess := random CLIENT_ID_LIMIT_ - if clients_.contains guess: continue - clients_[guess] = pid - return guess - -class ServiceRpcBroker_ extends broker.RpcBroker: - pids_ ::= {} - - accept gid/int pid/int -> bool: - return pids_.contains pid - - add_process pid/int -> none: - pids_.add pid - - remove_process pid/int -> none: - pids_.remove pid - cancel_requests pid + guess := random RANDOM_ID_LIMIT_ + if not map.contains guess: return guess diff --git a/lib/system/trace.toit b/lib/system/trace.toit index 04eb63198..9270ae7a5 100644 --- a/lib/system/trace.toit +++ b/lib/system/trace.toit @@ -21,7 +21,7 @@ send_trace_message message/ByteArray -> none: if service: handled = service.handle_trace message else: - service = (TraceServiceClient --no-open).open + service = (TraceServiceClient).open --if_absent=: null if service: handled = service.handle_trace message service_ = service diff --git a/system/containers.toit b/system/containers.toit index 2dc83cd3a..9287b6581 100644 --- a/system/containers.toit +++ b/system/containers.toit @@ -21,7 +21,7 @@ import encoding.base64 import encoding.tison import system.assets -import system.services show ServiceDefinition ServiceResource +import system.services show ServiceHandler ServiceProvider ServiceResource import system.api.containers show ContainerService import .flash.allocation @@ -77,8 +77,8 @@ class ContainerResource extends ServiceResource: container/Container hash_code/int ::= hash_code_next - constructor .container service/ServiceDefinition client/int: - super service client --notifiable + constructor .container provider/ServiceProvider client/int: + super provider client --notifiable container.resources.add this static hash_code_next_/int := 0 @@ -175,11 +175,11 @@ class ContainerImageFlash extends ContainerImage: finally: manager.image_registry.free allocation -abstract class ContainerServiceDefinition extends ServiceDefinition - implements ContainerService: +abstract class ContainerServiceProvider extends ServiceProvider + implements ContainerService ServiceHandler: constructor: super "system/containers" --major=0 --minor=2 - provides ContainerService.UUID ContainerService.MAJOR ContainerService.MINOR + provides ContainerService.SELECTOR --handler=this install handle pid/int client/int index/int arguments/any -> any: @@ -261,7 +261,7 @@ abstract class ContainerServiceDefinition extends ServiceDefinition image := add_flash_image allocation return image.id -class ContainerManager extends ContainerServiceDefinition implements SystemMessageHandler_: +class ContainerManager extends ContainerServiceProvider implements SystemMessageHandler_: image_registry/FlashRegistry ::= ? service_manager_/SystemServiceManager ::= ? diff --git a/system/extensions/esp32/boot.toit b/system/extensions/esp32/boot.toit index 3eef36b64..0c06c7acc 100644 --- a/system/extensions/esp32/boot.toit +++ b/system/extensions/esp32/boot.toit @@ -43,8 +43,8 @@ class SystemImage extends ContainerImage: main: container_manager ::= initialize_system [ - FirmwareServiceDefinition, - WifiServiceDefinition + FirmwareServiceProvider, + WifiServiceProvider ] container_manager.register_system_image SystemImage container_manager diff --git a/system/extensions/esp32/firmware.toit b/system/extensions/esp32/firmware.toit index 5ec90f0f0..d6e8442a2 100644 --- a/system/extensions/esp32/firmware.toit +++ b/system/extensions/esp32/firmware.toit @@ -14,13 +14,13 @@ // directory of this repository. import system.api.firmware show FirmwareService -import system.services show ServiceDefinition ServiceResource -import system.base.firmware show FirmwareServiceDefinitionBase FirmwareWriter +import system.services show ServiceProvider ServiceResource +import system.base.firmware show FirmwareServiceProviderBase FirmwareWriter import esp32 import encoding.ubjson -class FirmwareServiceDefinition extends FirmwareServiceDefinitionBase: +class FirmwareServiceProvider extends FirmwareServiceProviderBase: config_/Map ::= {:} constructor: @@ -78,10 +78,10 @@ class FirmwareWriter_ extends ServiceResource implements FirmwareWriter: fullness_/int := 0 written_/int := ? - constructor service/ServiceDefinition client/int from/int to/int: + constructor provider/ServiceProvider client/int from/int to/int: ota_begin_ from to written_ = from - super service client + super provider client write bytes/ByteArray -> int: return write_ bytes.size: | index from to | diff --git a/system/extensions/esp32/wifi.toit b/system/extensions/esp32/wifi.toit index 9c07cf964..f285756b2 100644 --- a/system/extensions/esp32/wifi.toit +++ b/system/extensions/esp32/wifi.toit @@ -25,12 +25,12 @@ import system.firmware import system.api.wifi show WifiService import system.api.network show NetworkService -import system.services show ServiceDefinition ServiceResource +import system.services show ServiceResource import system.base.network show NetworkModule NetworkResource NetworkState import ..shared.network_base -class WifiServiceDefinition extends NetworkServiceDefinitionBase: +class WifiServiceProvider extends NetworkServiceProviderBase: static WIFI_CONFIG_STORE_KEY ::= "system/wifi" state_/NetworkState ::= NetworkState @@ -38,7 +38,7 @@ class WifiServiceDefinition extends NetworkServiceDefinitionBase: constructor: super "system/wifi/esp32" --major=0 --minor=1 - provides WifiService.UUID WifiService.MAJOR WifiService.MINOR + provides WifiService.SELECTOR --handler=this handle pid/int client/int index/int arguments/any -> any: if index == WifiService.CONNECT_INDEX: @@ -160,7 +160,7 @@ class WifiModule implements NetworkModule: static WIFI_DHCP_TIMEOUT_ ::= Duration --s=16 logger_/log.Logger ::= log.default.with_name "wifi" - service/WifiServiceDefinition + service/WifiServiceProvider // TODO(kasper): Consider splitting the AP and non-AP case out // into two subclasses. diff --git a/system/extensions/host/initialize.toit b/system/extensions/host/initialize.toit index b89133005..50ab3d14c 100644 --- a/system/extensions/host/initialize.toit +++ b/system/extensions/host/initialize.toit @@ -18,5 +18,5 @@ import ...containers import ...initialize initialize_host -> ContainerManager: - network := NetworkServiceDefinition + network := NetworkServiceProvider return initialize_system [network] diff --git a/system/extensions/host/network.toit b/system/extensions/host/network.toit index bf302b175..d6fe69f67 100644 --- a/system/extensions/host/network.toit +++ b/system/extensions/host/network.toit @@ -16,12 +16,12 @@ import net import net.modules.udp -import system.services show ServiceDefinition ServiceResource +import system.services show ServiceProvider ServiceResource import system.api.network show NetworkService import ..shared.network_base -class NetworkServiceDefinition extends NetworkServiceDefinitionBase: +class NetworkServiceProvider extends NetworkServiceProviderBase: constructor: super "system/network/host" --major=0 --minor=1 @@ -30,8 +30,8 @@ class NetworkServiceDefinition extends NetworkServiceDefinitionBase: return [resource.serialize_for_rpc, NetworkService.PROXY_NONE] class NetworkResource extends ServiceResource: - constructor service/ServiceDefinition client/int: - super service client + constructor provider/ServiceProvider client/int: + super provider client on_closed -> none: // Do nothing. diff --git a/system/extensions/shared/network_base.toit b/system/extensions/shared/network_base.toit index cb9e044e7..a4dff983e 100644 --- a/system/extensions/shared/network_base.toit +++ b/system/extensions/shared/network_base.toit @@ -13,13 +13,14 @@ // The license can be found in the file `LICENSE` in the top level // directory of this repository. -import system.services show ServiceDefinition ServiceResource +import system.services show ServiceHandler ServiceProvider ServiceResource import system.api.network show NetworkService -abstract class NetworkServiceDefinitionBase extends ServiceDefinition implements NetworkService: +abstract class NetworkServiceProviderBase extends ServiceProvider + implements NetworkService ServiceHandler: constructor name/string --major/int --minor/int: super name --major=major --minor=minor - provides NetworkService.UUID NetworkService.MAJOR NetworkService.MINOR + provides NetworkService.SELECTOR --handler=this handle pid/int client/int index/int arguments/any -> any: if index == NetworkService.CONNECT_INDEX: diff --git a/system/flash/image_writer.toit b/system/flash/image_writer.toit index 6669a4f12..87458a4f0 100644 --- a/system/flash/image_writer.toit +++ b/system/flash/image_writer.toit @@ -15,7 +15,7 @@ import binary import uuid -import system.services show ServiceResource ServiceDefinition +import system.services show ServiceProvider ServiceResource import .allocation import .registry @@ -33,9 +33,9 @@ class ContainerImageWriter extends ServiceResource: partial_chunk_/ByteArray? := ByteArray IMAGE_CHUNK_SIZE partial_chunk_fill_/int := 0 - constructor service/ServiceDefinition client/int .reservation_: + constructor provider/ServiceProvider client/int .reservation_: image_ = image_writer_create_ reservation_.offset reservation_.size - super service client + super provider client write data/ByteArray -> none: List.chunk_up 0 data.size (IMAGE_CHUNK_SIZE - partial_chunk_fill_) IMAGE_CHUNK_SIZE: | from to size | diff --git a/system/initialize.toit b/system/initialize.toit index 8ab7365f7..a2f4cfc39 100644 --- a/system/initialize.toit +++ b/system/initialize.toit @@ -13,7 +13,7 @@ // The license can be found in the file `LICENSE` in the top level // directory of this repository. -import system.services show ServiceDefinition +import system.services show ServiceProvider import .flash.registry import .containers @@ -26,5 +26,5 @@ Initialize the system and create the all important $ContainerManager initialize_system extensions/List -> ContainerManager: flash_registry ::= FlashRegistry.scan service_manager ::= SystemServiceManager - extensions.do: | service/ServiceDefinition | service.install + extensions.do: | provider/ServiceProvider | provider.install return ContainerManager flash_registry service_manager diff --git a/system/services.toit b/system/services.toit index 95210e9d7..61189ee24 100644 --- a/system/services.toit +++ b/system/services.toit @@ -13,84 +13,144 @@ // The license can be found in the file `LICENSE` in the top level // directory of this repository. -import system.services - show - ServiceDefinition - SERVICES_MANAGER_NOTIFY_ADD_PROCESS - SERVICES_MANAGER_NOTIFY_REMOVE_PROCESS - import monitor +import system.services show ServiceProvider ServiceHandler import system.api.service_discovery show ServiceDiscoveryService -class SystemServiceManager extends ServiceDefinition implements ServiceDiscoveryService: +class DiscoverableService: + pid/int + id/int + uuid/string + name/string + major/int + minor/int + priority/int + tags/List? + constructor --.pid --.id --.uuid --.name --.major --.minor --.priority --.tags: + +class SystemServiceManager extends ServiceProvider implements ServiceDiscoveryService ServiceHandler: service_managers_/Map ::= {:} // Map> - services_by_pid_/Map ::= {:} // Map> - services_by_uuid_/Map ::= {:} // Map + + services_by_pid_/Map ::= {:} // Map>> + services_by_uuid_/Map ::= {:} // Map> signal_/monitor.Signal ::= monitor.Signal constructor: super "system/service-discovery" --major=0 --minor=1 --patch=1 - provides ServiceDiscoveryService.UUID ServiceDiscoveryService.MAJOR ServiceDiscoveryService.MINOR + provides ServiceDiscoveryService.SELECTOR + --handler=this + --id=0 install handle pid/int client/int index/int arguments/any -> any: if index == ServiceDiscoveryService.DISCOVER_INDEX: - return discover arguments[0] arguments[1] pid + return discover arguments[0] --wait=arguments[1] + if index == ServiceDiscoveryService.WATCH_INDEX: + return watch pid arguments if index == ServiceDiscoveryService.LISTEN_INDEX: - return listen arguments pid + return listen pid arguments if index == ServiceDiscoveryService.UNLISTEN_INDEX: - return unlisten arguments + return unlisten pid arguments unreachable - listen uuid/string pid/int -> none: - if services_by_uuid_.contains uuid: - throw "Already registered service:$uuid" - services_by_uuid_[uuid] = pid + listen pid/int arguments/List -> none: + services := services_by_pid_.get pid --init=(: {:}) + id := arguments[0] + if services.contains id: throw "Service id $id is already in use" + + uuid := arguments[1] + service := DiscoverableService + --pid=pid + --id=id + --uuid=uuid + --name=arguments[2] + --major=arguments[3] + --minor=arguments[4] + --priority=arguments[5] + --tags=arguments[6] + services[id] = service + + // Register the service based on its uuid and sort the all services + // with the same uuid by descending priority. + uuids := services_by_uuid_.get uuid --init=(: []) + uuids.add service + uuids.sort --in_place: | a b | b.priority.compare_to a.priority + + // Register the process as a service manager and signal + // anyone waiting for services to appear. service_managers_.get pid --init=(: {}) - uuids := services_by_pid_.get pid --init=(: {}) - uuids.add uuid signal_.raise - unlisten uuid/string -> none: - pid := services_by_uuid_.get uuid - if not pid: return - services_by_uuid_.remove uuid - uuids := services_by_pid_.get pid - if not uuids: return - uuids.remove uuid - if not uuids.is_empty: return + unlisten pid/int id/int -> none: + services := services_by_pid_.get pid + if not services: return + service := services.get id + if not service: return + services.remove id + + uuid := service.uuid + uuids := services_by_uuid_.get uuid + if uuids: + uuids.remove service + if uuids.is_empty: services_by_uuid_.remove uuid + + if not services.is_empty: return service_managers_.remove pid services_by_pid_.remove pid - discover uuid/string wait/bool pid/int -> int?: - target/int? := null + discover uuid/string --wait/bool -> List?: + services/List? := null if wait: signal_.wait: - target = services_by_uuid_.get uuid - target != null + services = services_by_uuid_.get uuid + services != null else: - target = services_by_uuid_.get uuid - if not target: return null - processes := service_managers_[target] - if processes: - processes.add pid - process_send_ target SYSTEM_RPC_NOTIFY_ [SERVICES_MANAGER_NOTIFY_ADD_PROCESS, pid] - return target + services = services_by_uuid_.get uuid + if not services: return null + + // TODO(kasper): Consider keeping the list of + // services in a form that is ready to send + // back without any transformations. + result := Array_ 7 * services.size + index := 0 + services.do: | service/DiscoverableService | + result[index++] = service.pid + result[index++] = service.id + result[index++] = service.name + result[index++] = service.major + result[index++] = service.minor + result[index++] = service.priority + result[index++] = service.tags + return result + + watch pid/int target/int -> none: + if pid == target: return + processes := service_managers_.get pid + if processes: processes.add target on_process_stop pid/int -> none: - uuids := services_by_pid_.get pid - // Iterate over a copy of the uuids, so we can manipulate the - // underlying set in the call to unlisten. - if uuids: (Array_.from uuids).do: unlisten it + services := services_by_pid_.get pid + // Iterate over a copy of the values, so we can manipulate the + // underlying map in the call to unlisten. + if services: services.values.do: | service/DiscoverableService | + unlisten service.pid service.id // Tell service managers about the termination. service_managers_.do: | manager/int processes/Set | if not processes.contains pid: continue.do processes.remove pid - process_send_ manager SYSTEM_RPC_NOTIFY_ [SERVICES_MANAGER_NOTIFY_REMOVE_PROCESS, pid] + process_send_ manager SYSTEM_RPC_NOTIFY_TERMINATED_ pid + + listen id/int uuid/string -> none + --name/string + --major/int + --minor/int + --priority/int + --tags/List: + unreachable // <-- TODO(kasper): nasty - discover uuid/string wait/bool -> int?: + unlisten id/int -> none: unreachable // <-- TODO(kasper): nasty - listen uuid/string -> none: + watch target/int -> none: unreachable // <-- TODO(kasper): nasty diff --git a/tests/firmware_map_test.toit b/tests/firmware_map_test.toit index 7211df111..ebec1d33a 100644 --- a/tests/firmware_map_test.toit +++ b/tests/firmware_map_test.toit @@ -4,7 +4,7 @@ import expect import system.firmware -import system.base.firmware show FirmwareServiceDefinitionBase FirmwareWriter +import system.base.firmware show FirmwareServiceProviderBase FirmwareWriter main: test_simple_mapping @@ -21,7 +21,7 @@ test_map: // into issues where the current process caches // the service client. - service := FirmwareServiceDefinition + service := FirmwareServiceProvider service.install block_called := false @@ -93,7 +93,7 @@ test_mapping bytes/ByteArray mapping/firmware.FirmwareMapping: test_mapping bytes[..split] mapping[..split] test_mapping bytes[split..] mapping[split..] -class FirmwareServiceDefinition extends FirmwareServiceDefinitionBase: +class FirmwareServiceProvider extends FirmwareServiceProviderBase: content/ByteArray? := null constructor: diff --git a/tests/log_test.toit b/tests/log_test.toit index 93f6b8aaf..52551be36 100644 --- a/tests/log_test.toit +++ b/tests/log_test.toit @@ -7,7 +7,7 @@ import system.api.print import .services_print_test show PrintServiceDefinition import expect show * -service/PrintServiceDefinition ::= PrintServiceDefinition +service ::= PrintServiceDefinition expect output/string? [block]: expect_equals 0 service.messages.size diff --git a/tests/negative/gold/illegal_system_call_test.gold b/tests/negative/gold/illegal_system_call_test.gold index ad13afa5c..de8b4de13 100644 --- a/tests/negative/gold/illegal_system_call_test.gold +++ b/tests/negative/gold/illegal_system_call_test.gold @@ -1,7 +1,7 @@ As check failed: null is not a ServiceResource. - 0: ServiceDefinition.resource /system/services.toit:170:5 - 1: ContainerServiceDefinition.handle <...>/system/containers.toit:201:19 - 2: ServiceManager_. /system/services.toit:357:15 + 0: ServiceProvider.resource /system/services.toit:385:5 + 1: ContainerServiceProvider.handle <...>/system/containers.toit:201:19 + 2: ServiceManager_. /system/services.toit:609:15 3: RpcRequest_.process. /rpc/broker.toit:98:26 4: RpcRequest_.process /rpc/broker.toit:95:3 5: RpcRequestQueue_.ensure_processing_task_... /rpc/broker.toit:214:20 diff --git a/tests/negative/gold/invalid_program_test.gold b/tests/negative/gold/invalid_program_test.gold index 1ef89cc0f..39bf1f417 100644 --- a/tests/negative/gold/invalid_program_test.gold +++ b/tests/negative/gold/invalid_program_test.gold @@ -1,4 +1,4 @@ INVALID_PROGRAM error. -2622 +2632 0: run_global_initializer_ /core/objects.toit:254:1 1: main tests/negative/invalid_program_test.toit:9:3 diff --git a/tests/negative/illegal_system_call_test.toit b/tests/negative/illegal_system_call_test.toit index 76b330436..91f460b6f 100644 --- a/tests/negative/illegal_system_call_test.toit +++ b/tests/negative/illegal_system_call_test.toit @@ -7,6 +7,7 @@ import system.api.containers show ContainerServiceClient main: client := ContainerServiceClient + client.open // This is an illegal call, which will cause the system process to throw // an exception. This exception is returned over the process boundary via // the RPC mechanism. diff --git a/tests/negative/services_trace_fail_test.toit b/tests/negative/services_trace_fail_test.toit index 480acbcaf..7f89efcfd 100644 --- a/tests/negative/services_trace_fail_test.toit +++ b/tests/negative/services_trace_fail_test.toit @@ -14,7 +14,7 @@ main: class TraceServiceDefinition extends ServiceDefinition implements TraceService: constructor: super "system/trace/test" --major=1 --minor=2 - provides TraceService.UUID TraceService.MAJOR TraceService.MINOR + provides TraceService.SELECTOR handle pid/int client/int index/int arguments/any -> any: if index == TraceService.HANDLE_TRACE_INDEX: diff --git a/tests/negative/services_trace_unhandled_test.toit b/tests/negative/services_trace_unhandled_test.toit index 6cf359f4f..4447ee738 100644 --- a/tests/negative/services_trace_unhandled_test.toit +++ b/tests/negative/services_trace_unhandled_test.toit @@ -14,7 +14,7 @@ main: class TraceServiceDefinition extends ServiceDefinition implements TraceService: constructor: super "system/trace/test" --major=1 --minor=2 - provides TraceService.UUID TraceService.MAJOR TraceService.MINOR + provides TraceService.SELECTOR handle pid/int client/int index/int arguments/any -> any: if index == TraceService.HANDLE_TRACE_INDEX: diff --git a/tests/negative/services_trace_uninstalled_test.toit b/tests/negative/services_trace_uninstalled_test.toit index bad91d298..8f2acf0b9 100644 --- a/tests/negative/services_trace_uninstalled_test.toit +++ b/tests/negative/services_trace_uninstalled_test.toit @@ -16,7 +16,7 @@ main: class TraceServiceDefinition extends ServiceDefinition implements TraceService: constructor: super "system/trace/test" --major=1 --minor=2 - provides TraceService.UUID TraceService.MAJOR TraceService.MINOR + provides TraceService.SELECTOR handle pid/int client/int index/int arguments/any -> any: if index == TraceService.HANDLE_TRACE_INDEX: diff --git a/tests/process_test_slow.toit b/tests/process_test_slow.toit index a591a38aa..de5860571 100644 --- a/tests/process_test_slow.toit +++ b/tests/process_test_slow.toit @@ -34,11 +34,10 @@ test_priorities: test_priority n --low=Process.PRIORITY_NORMAL --high=Process.PRIORITY_CRITICAL test_priority n --low=Process.PRIORITY_HIGH --high=Process.PRIORITY_CRITICAL - test_priority n/int --low/int --high/int: Process.current.priority = Process.PRIORITY_CRITICAL print "$n x [$low < $high]" - service := RegistrationServiceDefinition + service := RegistrationServiceProvider service.install baseline := calibrate service @@ -66,7 +65,7 @@ test_priority n/int --low/int --high/int: Process.current.priority = Process.PRIORITY_NORMAL -calibrate service/RegistrationServiceDefinition -> int: +calibrate service/RegistrationServiceProvider -> int: begin := Time.monotonic_us + 100_000 end := begin + 500_000 counts := service.wait 1: process begin end @@ -98,9 +97,10 @@ fib n: // ------------------------------------------------------------------ interface RegistrationService: - static UUID/string ::= "82bcb411-e479-485e-9a9e-81031a5137b2" - static MAJOR/int ::= 1 - static MINOR/int ::= 0 + static SELECTOR ::= services.ServiceSelector + --uuid="82bcb411-e479-485e-9a9e-81031a5137b2" + --major=1 + --minor=0 register who/int count/int -> none static REGISTER_INDEX ::= 0 @@ -108,29 +108,27 @@ interface RegistrationService: // ------------------------------------------------------------------ class RegistrationServiceClient extends services.ServiceClient implements RegistrationService: - constructor --open/bool=true: - super --open=open + static SELECTOR ::= RegistrationService.SELECTOR + constructor selector/services.ServiceSelector=SELECTOR: + assert: selector.matches SELECTOR + super selector open -> RegistrationServiceClient?: - client := open_ - RegistrationService.UUID - RegistrationService.MAJOR - RegistrationService.MINOR - --timeout=(Duration --s=1) // Use higher than usual timeout. - return client and this + return (super --timeout=(Duration --s=2)) and this // Use higher than usual timeout. register who/int count/int -> none: invoke_ RegistrationService.REGISTER_INDEX [who, count] // ------------------------------------------------------------------ -class RegistrationServiceDefinition extends services.ServiceDefinition implements RegistrationService: +class RegistrationServiceProvider extends services.ServiceProvider + implements services.ServiceHandler RegistrationService: counts_/Map? := null signal_/monitor.Signal ::= monitor.Signal constructor: super "log" --major=1 --minor=0 - provides RegistrationService.UUID RegistrationService.MAJOR RegistrationService.MINOR + provides RegistrationService.SELECTOR --handler=this handle pid/int client/int index/int arguments/any -> any: if index == RegistrationService.REGISTER_INDEX: diff --git a/tests/services_extension_test.toit b/tests/services_extension_test.toit index ca6478540..bf60f59a9 100644 --- a/tests/services_extension_test.toit +++ b/tests/services_extension_test.toit @@ -10,19 +10,19 @@ interface MyService: static MAJOR/int ::= 0 static MINOR/int ::= 1 - static FOO_INDEX ::= 0 foo -> int + static FOO_INDEX ::= 0 - static BAR_INDEX ::= 1 bar x/int -> int + static BAR_INDEX ::= 1 interface MyServiceExtended extends MyService: static UUID/string ::= "711e9020-69cd-4e86-84c7-6e0a92a26fa6" static MAJOR/int ::= 1 static MINOR/int ::= 2 - static BAZ_INDEX ::= 100 baz x/string -> none + static BAZ_INDEX ::= 100 main: spawn:: run_server diff --git a/tests/service_log_test.toit b/tests/services_log_test.toit similarity index 97% rename from tests/service_log_test.toit rename to tests/services_log_test.toit index 14fc1e244..d69022651 100644 --- a/tests/service_log_test.toit +++ b/tests/services_log_test.toit @@ -70,7 +70,7 @@ class LogServiceDefinition extends ServiceDefinition implements LogService: constructor: super "system/log/test" --major=1 --minor=2 - provides LogService.UUID LogService.MAJOR LogService.MINOR + provides LogService.SELECTOR handle pid/int client/int index/int arguments/any -> any: if index == LogService.LOG_INDEX: return logs_.add arguments diff --git a/tests/services_multi_test.toit b/tests/services_multi_test.toit new file mode 100644 index 000000000..87c288c80 --- /dev/null +++ b/tests/services_multi_test.toit @@ -0,0 +1,289 @@ +// Copyright (C) 2023 Toitware ApS. +// Use of this source code is governed by a Zero-Clause BSD license that can +// be found in the tests/LICENSE file. + +import system.services +import expect show * + +interface PingService: + static SELECTOR ::= services.ServiceSelector + --uuid="efc8fd7d-62ba-44bd-b215-5a819604aa28" + --major=7 + --minor=9 + + ping -> none + static PING_INDEX ::= 0 + +main: + test_positive + test_negative + test_errors + +test_positive: + tests_started := 0 + tests_run := 0 + + tests_started++ + with_installed_services --priority_a=30 --priority_b=20: + client := PingServiceClient + client.open + client.ping + expect_equals "ping/A" client.name + tests_run++ + expect_equals tests_started tests_run + + tests_started++ + with_installed_services --priority_a=20 --priority_b=30: + client := PingServiceClient + client.open + client.ping + expect_equals "ping/B" client.name + tests_run++ + expect_equals tests_started tests_run + + tests_started++ + with_client (PingService.SELECTOR.restrict.allow --name="ping/B"): | client/PingServiceClient | + expect_equals "ping/B" client.name + tests_run++ + expect_equals tests_started tests_run + + tests_started++ + with_client (PingService.SELECTOR.restrict.allow --name="ping/B" --major=3): | client/PingServiceClient | + expect_equals "ping/B" client.name + tests_run++ + expect_equals tests_started tests_run + + tests_started++ + with_client (PingService.SELECTOR.restrict.allow --name="ping/B" --major=3 --minor=4): | client/PingServiceClient | + expect_equals "ping/B" client.name + tests_run++ + expect_equals tests_started tests_run + + tests_started++ + with_client (PingService.SELECTOR.restrict.allow --tag="A"): | client/PingServiceClient | + expect_equals "ping/A" client.name + tests_run++ + expect_equals tests_started tests_run + + tests_started++ + with_client (PingService.SELECTOR.restrict.allow --tag="B"): | client/PingServiceClient | + expect_equals "ping/B" client.name + tests_run++ + expect_equals tests_started tests_run + + tests_started++ + with_client (PingService.SELECTOR.restrict.allow --tag="!A"): | client/PingServiceClient | + expect_equals "ping/B" client.name + tests_run++ + expect_equals tests_started tests_run + + // Look for either the non-exisiting 'nope' tag or B. + tests_started++ + selector := PingService.SELECTOR.restrict.allow --tags=["nope", "B"] + with_client selector: | client/PingServiceClient | + expect_equals "ping/B" client.name + tests_run++ + expect_equals tests_started tests_run + + tests_started++ + with_client (PingService.SELECTOR.restrict.deny --name="ping/A"): | client/PingServiceClient | + expect_equals "ping/B" client.name + tests_run++ + expect_equals tests_started tests_run + + tests_started++ + with_client (PingService.SELECTOR.restrict.deny --tag="A"): | client/PingServiceClient | + expect_equals "ping/B" client.name + tests_run++ + expect_equals tests_started tests_run + + tests_started++ + with_client (PingService.SELECTOR.restrict.deny --name="ping/B" --major=3): | client/PingServiceClient | + expect_equals "ping/A" client.name + tests_run++ + expect_equals tests_started tests_run + + tests_started++ + with_client (PingService.SELECTOR.restrict.deny --name="ping/B" --major=3 --minor=4): | client/PingServiceClient | + expect_equals "ping/A" client.name + tests_run++ + expect_equals tests_started tests_run + + tests_started++ + selector = PingService.SELECTOR.restrict + selector.allow --name="ping/B" --major=3 --minor=4 + selector.deny --name="ping/B" --major=33 --minor=44 + with_client selector: | client/PingServiceClient | + expect_equals "ping/B" client.name + tests_run++ + expect_equals tests_started tests_run + + tests_started++ + selector = PingService.SELECTOR.restrict + selector.allow --name="ping/B" --major=33 --minor=44 + selector.allow --name="ping/B" --major=3 --minor=4 + selector.allow --name="ping/B" --major=333 --minor=444 + with_client selector: | client/PingServiceClient | + expect_equals "ping/B" client.name + tests_run++ + expect_equals tests_started tests_run + + tests_started++ + selector.allow --name="ping/A" --major=11 --minor=22 + selector.allow --name="ping/A" --major=1 --minor=22 + selector.allow --name="ping/A" --major=11 --minor=2 + with_client selector: | client/PingServiceClient | + expect_equals "ping/B" client.name + tests_run++ + expect_equals tests_started tests_run + +test_negative: + expect_throw "Cannot disambiguate": + with_client PingService.SELECTOR: unreachable + + expect_throw "Cannot disambiguate": + with_client (PingService.SELECTOR.restrict.allow --tag="yada"): unreachable + + expect_throw "Cannot disambiguate": + with_client (PingService.SELECTOR.restrict.allow --tag="yada"): unreachable + + expect_throw "Cannot disambiguate": + with_client (PingService.SELECTOR.restrict.deny --name="ping/B" --major=33): unreachable + + expect_throw "Cannot disambiguate": + with_client (PingService.SELECTOR.restrict.deny --name="ping/B" --major=3 --minor=44): unreachable + + expect_throw "Cannot disambiguate": + with_client (PingService.SELECTOR.restrict.deny --name="ping/B" --major=33 --minor=44): unreachable + + expect_throw "Cannot disambiguate": + // Look for either tag B or yada. Both have yada. + selector := PingService.SELECTOR.restrict.allow --tags=["B", "yada"] + with_client selector: unreachable + + // Look for anything that doesn't have the non-existing tag 'nope'. + expect_throw "Cannot disambiguate": + with_client (PingService.SELECTOR.restrict.deny --tag="nope"): unreachable + + expect_throw "Cannot disambiguate": + selector := PingService.SELECTOR.restrict + selector.allow --name="ping/A" --major=11 --minor=22 + selector.allow --name="ping/A" --major=1 --minor=2 + selector.allow --name="ping/A" --major=111 --minor=222 + selector.allow --name="ping/B" --major=33 --minor=44 + selector.allow --name="ping/B" --major=3 --minor=4 + selector.allow --name="ping/B" --major=333 --minor=444 + with_client selector: unreachable + + expect_throw "Cannot find service": + with_client (PingService.SELECTOR.restrict.deny --tag="yada"): unreachable + + expect_throw "Cannot find service": + with_client (PingService.SELECTOR.restrict.allow --tag="nope"): unreachable + + expect_throw "Cannot find service": + // Look for tag B, but not yada. + selector := PingService.SELECTOR.restrict + selector.allow --tag="B" + selector.deny --tag="yada" + with_client selector: unreachable + + expect_throw "Cannot find service": + with_client (PingService.SELECTOR.restrict.allow --name="ping/B" --major=33): unreachable + + expect_throw "Cannot find service": + with_client (PingService.SELECTOR.restrict.allow --name="ping/B" --major=33 --minor=4): unreachable + + expect_throw "Cannot find service": + with_client (PingService.SELECTOR.restrict.allow --name="ping/B" --major=3 --minor=44): unreachable + +test_errors: + expect_throw "Must have major version to match on minor": + selector := PingService.SELECTOR.restrict + selector.allow --name="fusk" --minor=2 + + expect_throw "Must have major version to match on minor": + selector := PingService.SELECTOR.restrict + selector.deny --name="fusk" --minor=2 + + expect_throw "Cannot have multiple entries for the same named version": + selector := PingService.SELECTOR.restrict + selector.allow --name="fusk" + selector.allow --name="fusk" --major=1 + + expect_throw "Cannot have multiple entries for the same named version": + selector := PingService.SELECTOR.restrict + selector.allow --name="fusk" + selector.allow --name="fusk" --major=1 --minor=2 + + expect_throw "Cannot have multiple entries for the same named version": + selector := PingService.SELECTOR.restrict + selector.allow --name="fusk" + selector.deny --name="fusk" + + expect_throw "Cannot allow and deny the same tag": + selector := PingService.SELECTOR.restrict + selector.allow --tag="kuks" + selector.deny --tag="kuks" + +with_client selector/services.ServiceSelector [block]: + with_installed_services: + client := PingServiceClient selector + client.open + client.ping + block.call client + +with_installed_services --priority_a/int?=null --priority_b/int?=null [block]: + with_installed_services + --create_a=(: PingServiceProvider.A --priority=priority_a) + --create_b=(: PingServiceProvider.B --priority=priority_b) + block + +with_installed_services [--create_a] [--create_b] [block]: + service_a := create_a.call + service_a.install + service_b := create_b.call + service_b.install + + try: + block.call + finally: + service_a.uninstall + service_b.uninstall + +// ------------------------------------------------------------------ + +class PingServiceClient extends services.ServiceClient implements PingService: + constructor selector/services.ServiceSelector=PingService.SELECTOR: + super selector + + ping -> none: + invoke_ PingService.PING_INDEX null + +// ------------------------------------------------------------------ + +class PingServiceProvider extends services.ServiceProvider: + constructor.A --priority/int?=null: + super "ping/A" --major=1 --minor=2 --patch=5 --tags=["yada"] + provides PingService.SELECTOR + --handler=PingHandler "A" + --priority=priority + --tags=["A", "!B"] + + constructor.B --priority/int?=null: + super "ping/B" --major=3 --minor=4 --patch=17 --tags=["yada"] + provides PingService.SELECTOR + --handler=PingHandler "B" + --priority=priority + --tags=["!A", "B"] + +class PingHandler implements services.ServiceHandler PingService: + identifier/string + constructor .identifier: + + handle pid/int client/int index/int arguments/any -> any: + if index == PingService.PING_INDEX: return ping + unreachable + + ping -> none: + print "Ping $identifier" diff --git a/tests/services_network_test.toit b/tests/services_network_test.toit index 6e5014c9a..1acad4061 100644 --- a/tests/services_network_test.toit +++ b/tests/services_network_test.toit @@ -8,21 +8,25 @@ import net.tcp import writer import expect -import system.services show ServiceDefinition ServiceResource +import system.services show ServiceSelector ServiceResource import system.api.network show NetworkService NetworkServiceClient -import system.base.network show ProxyingNetworkServiceDefinition +import system.base.network show ProxyingNetworkServiceProvider -service_/NetworkServiceClient? ::= (FakeNetworkServiceClient --no-open).open +FAKE_TAG ::= "fake-$(random 1024)" +FAKE_SELECTOR ::= NetworkService.SELECTOR.restrict.allow --tag=FAKE_TAG + +service_/NetworkServiceClient? ::= (NetworkServiceClient FAKE_SELECTOR).open + --if_absent=: null main: - service := FakeNetworkServiceDefinition + service := FakeNetworkServiceProvider service.install test_address service test_resolve service test_tcp service service.uninstall -test_address service/FakeNetworkServiceDefinition: +test_address service/FakeNetworkServiceProvider: local_address ::= net.open.address service.address = null expect.expect_equals local_address open_fake.address @@ -34,7 +38,7 @@ test_address service/FakeNetworkServiceDefinition: expect.expect_equals "7.8.9.10" open_fake.address.stringify service.address = null -test_resolve service/FakeNetworkServiceDefinition: +test_resolve service/FakeNetworkServiceProvider: www_google ::= net.open.resolve "www.google.com" service.resolve = null expect.expect_list_equals www_google (open_fake.resolve "www.google.com") @@ -48,7 +52,7 @@ test_resolve service/FakeNetworkServiceDefinition: expect.expect_equals [net.IpAddress #[3, 4, 5, 6]] (open_fake.resolve "www.google.com") service.resolve = null -test_tcp service/FakeNetworkServiceDefinition: +test_tcp service/FakeNetworkServiceProvider: test_tcp_network open_fake service.enable_tcp_proxying test_tcp_network open_fake @@ -76,26 +80,17 @@ test_tcp_network network/net.Interface: open_fake -> net.Interface: return impl.SystemInterface_ service_ service_.connect -interface FakeNetworkService extends NetworkService: - static UUID /string ::= "5c6f4b05-5646-4866-856d-b12649ace896" - static MAJOR /int ::= 0 - static MINOR /int ::= 1 - -class FakeNetworkServiceClient extends NetworkServiceClient: - constructor --open/bool=true: - super --open=open - - open -> FakeNetworkServiceClient?: - return (open_ FakeNetworkService.UUID FakeNetworkService.MAJOR FakeNetworkService.MINOR) and this - -class FakeNetworkServiceDefinition extends ProxyingNetworkServiceDefinition: +class FakeNetworkServiceProvider extends ProxyingNetworkServiceProvider: proxy_mask_/int := 0 address_/ByteArray? := null resolve_/List? := null constructor: super "system/network/test" --major=1 --minor=2 // Major and minor versions do not matter here. - provides FakeNetworkService.UUID FakeNetworkService.MAJOR FakeNetworkService.MINOR + provides NetworkService.SELECTOR + --handler=this + --priority=10 // Lower than the default, so others do not find this. + --tags=[FAKE_TAG] proxy_mask -> int: return proxy_mask_ diff --git a/tests/services_notify_test.toit b/tests/services_notify_test.toit index 5ddaeddbf..f5a4ba285 100644 --- a/tests/services_notify_test.toit +++ b/tests/services_notify_test.toit @@ -11,11 +11,11 @@ interface ResourceService: static MAJOR/int ::= 1 static MINOR/int ::= 2 - static OPEN_INDEX ::= 0 open key/string -> int + static OPEN_INDEX ::= 0 - static NOTIFY_INDEX ::= 1 notify handle/int notification/any -> none + static NOTIFY_INDEX ::= 1 main: test_notify diff --git a/tests/services_print_test.toit b/tests/services_print_test.toit index 8241da211..750d355bb 100644 --- a/tests/services_print_test.toit +++ b/tests/services_print_test.toit @@ -28,7 +28,7 @@ class PrintServiceDefinition extends ServiceDefinition implements PrintService: constructor: super "system/print/test" --major=1 --minor=2 - provides PrintService.UUID PrintService.MAJOR PrintService.MINOR + provides PrintService.SELECTOR handle pid/int client/int index/int arguments/any -> any: if index == PrintService.PRINT_INDEX: return print arguments diff --git a/tests/services_resource_test.toit b/tests/services_resource_test.toit index b6e80b1f0..035d48815 100644 --- a/tests/services_resource_test.toit +++ b/tests/services_resource_test.toit @@ -10,14 +10,14 @@ interface ResourceService: static MAJOR/int ::= 1 static MINOR/int ::= 2 - static OPEN_INDEX ::= 0 open key/string -> int + static OPEN_INDEX ::= 0 - static MYCLOSE_INDEX ::= 1 myclose handle/int -> none + static MYCLOSE_INDEX ::= 1 - static CLOSE_COUNT_INDEX ::= 2 close_count key/string -> int + static CLOSE_COUNT_INDEX ::= 2 main: test_resources diff --git a/tests/services_simple_test.toit b/tests/services_simple_test.toit index f0f133e78..0cf2fb1d1 100644 --- a/tests/services_simple_test.toit +++ b/tests/services_simple_test.toit @@ -10,8 +10,8 @@ interface SimpleService: static MAJOR/int ::= 1 static MINOR/int ::= 2 - static LOG_INDEX ::= 0 log message/string -> none + static LOG_INDEX ::= 0 main: test_logging diff --git a/tests/services_trace_test.toit b/tests/services_trace_test.toit index ba26ae128..3f99b222e 100644 --- a/tests/services_trace_test.toit +++ b/tests/services_trace_test.toit @@ -27,7 +27,7 @@ class TraceServiceDefinition extends ServiceDefinition implements TraceService: constructor: super "system/trace/test" --major=1 --minor=2 - provides TraceService.UUID TraceService.MAJOR TraceService.MINOR + provides TraceService.SELECTOR handle pid/int client/int index/int arguments/any -> any: if index == TraceService.HANDLE_TRACE_INDEX: diff --git a/tests/tls_test_slow.toit b/tests/tls_test_slow.toit index 7680b5c71..a91a36ce2 100644 --- a/tests/tls_test_slow.toit +++ b/tests/tls_test_slow.toit @@ -64,7 +64,7 @@ run_tests: ] non_working := [ "$(dns_lookup "amazon.com")", // This fails because the name we use to connect (an IP address string) doesn't match the cert name. - "wrong.host.badssl.com/Common Name", + "wrong.host.badssl.com/Common Name|unknown root cert", "self-signed.badssl.com/Certificate verification failed|unknown root cert", "untrusted-root.badssl.com/Certificate verification failed|unknown root cert", // "revoked.badssl.com", // We don't have support for cert revocation yet.