From e3095412844691d2cf61590701db4d10379d2012 Mon Sep 17 00:00:00 2001 From: Alistair Buxton Date: Thu, 5 Apr 2018 19:01:29 +0100 Subject: [PATCH 1/3] Non-breaking, ifdef-wrappable changes for Bluetooth. --- CMakeLists.txt | 8 + src/lib/arch/IArchNetwork.h | 1 + src/lib/arch/unix/ArchNetworkBSD.cpp | 353 +++++++++++++++++++++++++-- src/lib/arch/unix/ArchNetworkBSD.h | 16 +- 4 files changed, 363 insertions(+), 15 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 5c4ba6de..c2961435 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -197,6 +197,7 @@ if (UNIX) check_library_exists ("Xinerama" XineramaQueryExtension "" HAVE_Xinerama) check_library_exists ("Xi" XISelectEvents "" HAVE_Xi) check_library_exists ("Xrandr" XRRQueryExtension "" HAVE_Xrandr) + check_library_exists ("bluetooth" hci_open_dev "" HAVE_BLUETOOTH) if (HAVE_ICE) @@ -241,6 +242,13 @@ if (UNIX) list (APPEND libs Xi) endif() + if (HAVE_BLUETOOTH) + list (APPEND libs bluetooth) + add_definitions( -DHAVE_BLUETOOTH ) + else() + message (Warning "Missing library: bluetooth, disabled") + endif() + endif() # For config.h, set some static values; it may be a good idea to make diff --git a/src/lib/arch/IArchNetwork.h b/src/lib/arch/IArchNetwork.h index b8595061..e9e6bb0c 100644 --- a/src/lib/arch/IArchNetwork.h +++ b/src/lib/arch/IArchNetwork.h @@ -65,6 +65,7 @@ public: kUNKNOWN, kINET, kINET6, + kBLUETOOTH, }; //! Supported socket types diff --git a/src/lib/arch/unix/ArchNetworkBSD.cpp b/src/lib/arch/unix/ArchNetworkBSD.cpp index 149218c8..20c8e39a 100644 --- a/src/lib/arch/unix/ArchNetworkBSD.cpp +++ b/src/lib/arch/unix/ArchNetworkBSD.cpp @@ -21,6 +21,7 @@ #include "arch/unix/ArchMultithreadPosix.h" #include "arch/unix/XArchUnix.h" #include "arch/Arch.h" +#include "base/Log.h" #if HAVE_UNISTD_H # include @@ -54,6 +55,7 @@ static const int s_family[] = { PF_UNSPEC, PF_INET, PF_INET6, + PF_BLUETOOTH }; static const int s_type[] = { SOCK_DGRAM, @@ -83,6 +85,220 @@ inet_aton(const char* cp, struct in_addr* inp) } #endif +#if HAVE_BLUETOOTH +# include +# include +# include +# include + +static const bdaddr_t bdaddr_any = {0, 0, 0, 0, 0, 0}; +static const bdaddr_t bdaddr_local = {0, 0, 0, 0xff, 0xff, 0xff}; +uint32_t service_uuid_int[] = { 0x0e92afb7, 0xaeca4c00, 0x950d6f96, 0xc3cd5b16 }; + +sdp_session_t *ArchNetworkBSD::register_service(uint8_t rfcomm_channel) +{ + const char *service_name = "Barrier"; + const char *service_dsc = "Mouse and keyboard sharing"; + const char *service_prov = "Barrier"; + + uuid_t root_uuid, l2cap_uuid, rfcomm_uuid, svc_uuid; + sdp_list_t *l2cap_list = 0, + *rfcomm_list = 0, + *root_list = 0, + *proto_list = 0, + *access_proto_list = 0; + sdp_data_t *channel = 0, *psm = 0; + + sdp_record_t *record = sdp_record_alloc(); + + // set the general service ID + sdp_uuid128_create( &svc_uuid, &service_uuid_int ); + sdp_set_service_id( record, svc_uuid ); + + // make the service record publicly browsable + sdp_uuid16_create(&root_uuid, PUBLIC_BROWSE_GROUP); + root_list = sdp_list_append(0, &root_uuid); + sdp_set_browse_groups( record, root_list ); + + // set l2cap information + sdp_uuid16_create(&l2cap_uuid, L2CAP_UUID); + l2cap_list = sdp_list_append( 0, &l2cap_uuid ); + proto_list = sdp_list_append( 0, l2cap_list ); + + // set rfcomm information + sdp_uuid16_create(&rfcomm_uuid, RFCOMM_UUID); + channel = sdp_data_alloc(SDP_UINT8, &rfcomm_channel); + rfcomm_list = sdp_list_append( 0, &rfcomm_uuid ); + sdp_list_append( rfcomm_list, channel ); + sdp_list_append( proto_list, rfcomm_list ); + + // attach protocol information to service record + access_proto_list = sdp_list_append( 0, proto_list ); + sdp_set_access_protos( record, access_proto_list ); + + // set the name, provider, and description + sdp_set_info_attr(record, service_name, service_prov, service_dsc); + + int err = 0; + sdp_session_t *session = 0; + + // connect to the local SDP server, register the service record, and + // disconnect + session = sdp_connect( &bdaddr_any, &bdaddr_local, SDP_RETRY_IF_BUSY ); + err = sdp_record_register(session, record, 0); + + // cleanup + sdp_data_free( channel ); + sdp_list_free( l2cap_list, 0 ); + sdp_list_free( rfcomm_list, 0 ); + sdp_list_free( root_list, 0 ); + sdp_list_free( access_proto_list, 0 ); + + return session; +} + +ArchNetAddress ArchNetworkBSD::find_channel(ArchNetAddress addr) { + struct sockaddr_rc *Baddr = reinterpret_cast(&addr->m_addr); + + LOG((CLOG_INFO "checking services on %s", addrToString(addr).c_str())); + + uuid_t svc_uuid; + int err; + sdp_list_t *response_list = NULL, *search_list, *attrid_list; + sdp_session_t *session = 0; + + // connect to the SDP server running on the remote machine + session = sdp_connect( &bdaddr_any, &Baddr->rc_bdaddr, SDP_RETRY_IF_BUSY ); + if(!session) { + throwError(errno); + } + + // specify the UUID of the application we're searching for + sdp_uuid128_create( &svc_uuid, &service_uuid_int ); + search_list = sdp_list_append( NULL, &svc_uuid ); + + // specify that we want a list of all the matching applications' attributes + uint32_t range = 0x0000ffff; + attrid_list = sdp_list_append( NULL, &range ); + + // get a list of service records that have UUID 0xabcd + err = sdp_service_search_attr_req( session, search_list, + SDP_ATTR_REQ_RANGE, attrid_list, &response_list); + + sdp_list_t *r = response_list; + + // go through each of the service records + for (; r; r = r->next ) { + sdp_record_t *rec = (sdp_record_t*) r->data; + sdp_list_t *proto_list; + + // get a list of the protocol sequences + if( sdp_get_access_protos( rec, &proto_list ) == 0 ) { + sdp_list_t *p = proto_list; + + // go through each protocol sequence + for( ; p ; p = p->next ) { + sdp_list_t *pds = (sdp_list_t*)p->data; + + // go through each protocol list of the protocol sequence + for( ; pds ; pds = pds->next ) { + + // check the protocol attributes + sdp_data_t *d = (sdp_data_t*)pds->data; + int proto = 0; + for( ; d; d = d->next ) { + switch( d->dtd ) { + case SDP_UUID16: + case SDP_UUID32: + case SDP_UUID128: + proto = sdp_uuid_to_proto( &d->val.uuid ); + break; + case SDP_UINT8: + if( proto == RFCOMM_UUID ) { + LOG((CLOG_INFO "Found synergy service on channel %d", d->val.int8)); + Baddr->rc_channel = d->val.int8; + } + break; + } + } + } + sdp_list_free( (sdp_list_t*)p->data, 0 ); + } + sdp_list_free( proto_list, 0 ); + } + sdp_record_free( rec ); + } + + sdp_close(session); + if(Baddr->rc_channel == 0) + throw XArchNetwork("Synergy service not available on remote device"); + + return addr; +} + +ArchNetAddress ArchNetworkBSD::find_service(ArchNetAddress addr) { + if(isAnyAddr(addr)) { + LOG((CLOG_INFO "Scanning for bluetooth devices...")); + inquiry_info *ii = NULL; + int max_rsp, num_rsp; + int dev_id, sock, len, flags; + int i; + char _addr[19] = { 0 }; + char name[248] = { 0 }; + + dev_id = hci_get_route(NULL); + sock = hci_open_dev( dev_id ); + if (dev_id < 0 || sock < 0) { + throwError(errno); + } + + len = 8; + max_rsp = 255; + flags = IREQ_CACHE_FLUSH; + ii = (inquiry_info*)malloc(max_rsp * sizeof(inquiry_info)); + + ArchNetAddress temp_addr = copyAddr(addr); + struct sockaddr_rc *temp_baddr = reinterpret_cast(&temp_addr->m_addr); + + num_rsp = hci_inquiry(dev_id, len, max_rsp, NULL, &ii, flags); + if( num_rsp < 0 ) perror("hci_inquiry"); + + for (i = 0; i < num_rsp; i++) { + ba2str(&(ii+i)->bdaddr, _addr); + memset(name, 0, sizeof(name)); + if (hci_read_remote_name(sock, &(ii+i)->bdaddr, sizeof(name), + name, 0) < 0) + strcpy(name, "[unknown]"); + LOG((CLOG_INFO "Found device: %s %s", _addr, name)); + + try { + temp_baddr->rc_bdaddr = (ii+i)->bdaddr; + find_channel(temp_addr); + } + catch(...) { + continue; + } + //found a device + *addr = *temp_addr; + break; + } + + delete temp_addr; + free( ii ); + hci_close_dev(sock); + + if(isAnyAddr(addr)) { + throw XArchNetwork("No devices with synergy service found"); + } + + return addr; + + } else + return find_channel(addr); +} + +#endif + // // ArchNetworkBSD // @@ -101,13 +317,20 @@ ArchNetworkBSD::init() { // create mutex to make some calls thread safe m_mutex = ARCH->newMutex(); +#if HAVE_BLUETOOTH + sdp_session = NULL; +#endif } ArchSocket ArchNetworkBSD::newSocket(EAddressFamily family, ESocketType type) { // create socket - int fd = socket(s_family[family], s_type[type], 0); + int protocol = 0; +#if HAVE_BLUETOOTH + if (family == kBLUETOOTH) protocol = BTPROTO_RFCOMM; +#endif + int fd = socket(s_family[family], s_type[type], protocol); if (fd == -1) { throwError(errno); } @@ -119,10 +342,21 @@ ArchNetworkBSD::newSocket(EAddressFamily family, ESocketType type) throw; } +#if HAVE_BLUETOOTH + if(family == kBLUETOOTH) { + int opt = 0; + opt |= RFCOMM_LM_AUTH; + opt |= RFCOMM_LM_ENCRYPT; + opt |= RFCOMM_LM_SECURE; + setsockopt(fd, SOL_RFCOMM, RFCOMM_LM, &opt, sizeof(opt)); + } +#endif + // allocate socket object ArchSocketImpl* newSocket = new ArchSocketImpl; newSocket->m_fd = fd; newSocket->m_refCount = 1; + newSocket->m_family = family; return newSocket; } @@ -143,6 +377,11 @@ ArchNetworkBSD::closeSocket(ArchSocket s) { assert(s != NULL); +#if HAVE_BLUETOOTH + if(s->m_family == kBLUETOOTH && sdp_session != NULL) + sdp_close(sdp_session); +#endif + // unref the socket and note if it should be released ARCH->lockMutex(m_mutex); const bool doClose = (--s->m_refCount == 0); @@ -267,6 +506,11 @@ ArchNetworkBSD::connectSocket(ArchSocket s, ArchNetAddress addr) assert(s != NULL); assert(addr != NULL); +#if HAVE_BLUETOOTH + if (getAddrFamily(addr) == kBLUETOOTH) + addr = find_service(addr); +#endif + if (connect(s->m_fd, TYPED_ADDR(struct sockaddr, addr), addr->m_len) == -1) { if (errno == EISCONN) { return true; @@ -662,6 +906,19 @@ ArchNetworkBSD::newAnyAddr(EAddressFamily family) addr->m_len = (socklen_t)sizeof(struct sockaddr_in6); break; } + +#if HAVE_BLUETOOTH + case kBLUETOOTH: { + struct sockaddr_rc* BAddr = + reinterpret_cast(&addr->m_addr); + BAddr->rc_family = AF_BLUETOOTH; + BAddr->rc_bdaddr = ((bdaddr_t) {{0, 0, 0, 0, 0, 0}}); + BAddr->rc_channel = (uint8_t) 0; + addr->m_len = sizeof(struct sockaddr_rc); + break; + } +#endif + default: delete addr; assert(0 && "invalid family"); @@ -726,24 +983,48 @@ ArchNetworkBSD::addrToName(ArchNetAddress addr) { assert(addr != NULL); - // mutexed name lookup (ugh) - ARCH->lockMutex(m_mutex); - char host[1024]; - char service[20]; - int ret = getnameinfo(TYPED_ADDR(struct sockaddr, addr), addr->m_len, host, - sizeof(host), service, sizeof(service), 0); - if (ret != 0) { + switch(getAddrFamily(addr)) { + case kINET: + case kINET6: { + // mutexed name lookup (ugh) + ARCH->lockMutex(m_mutex); + char host[1024]; + char service[20]; + int ret = getnameinfo(TYPED_ADDR(struct sockaddr, addr), addr->m_len, host, + sizeof(host), service, sizeof(service), 0); + if (ret != 0) { + ARCH->unlockMutex(m_mutex); + throwNameError(ret); + } + + // save (primary) name + std::string name = host; + + // done with static buffer ARCH->unlockMutex(m_mutex); - throwNameError(ret); + return name; } - // save (primary) name - std::string name = host; +#if HAVE_BLUETOOTH + case kBLUETOOTH: { + int sock = hci_open_dev( hci_get_route( NULL ) ); + struct sockaddr_rc *baddr = reinterpret_cast(&addr->m_addr); - // done with static buffer - ARCH->unlockMutex(m_mutex); + char cname[256] = "00:00:00:00:00:00"; + if(hci_read_remote_name(sock, &baddr->rc_bdaddr, 255, cname, 3000)) + return addrToString(addr); - return name; + close(sock); + + std::string name = cname; + return name; + } +#endif + + default: + assert(0 && "unknown address family"); + return ""; + } } std::string @@ -769,6 +1050,16 @@ ArchNetworkBSD::addrToString(ArchNetAddress addr) return strAddr; } +#if HAVE_BLUETOOTH + case kBLUETOOTH: { + struct sockaddr_rc *baddr = reinterpret_cast(&addr->m_addr); + char cstr[18] = "00:00:00:00:00:00"; + ba2str(&baddr->rc_bdaddr, cstr); + std::string s = cstr; + return s; + } +#endif + default: assert(0 && "unknown address family"); return ""; @@ -785,6 +1076,8 @@ ArchNetworkBSD::getAddrFamily(ArchNetAddress addr) return kINET; case AF_INET6: return kINET6; + case AF_BLUETOOTH: + return kBLUETOOTH; default: return kUNKNOWN; @@ -809,6 +1102,15 @@ ArchNetworkBSD::setAddrPort(ArchNetAddress addr, int port) break; } +#if HAVE_BLUETOOTH + case kBLUETOOTH: { + struct sockaddr_rc* ipAddr = + reinterpret_cast(&addr->m_addr); + ipAddr->rc_channel = (uint8_t) 0; + break; + } +#endif + default: assert(0 && "unknown address family"); break; @@ -831,6 +1133,14 @@ ArchNetworkBSD::getAddrPort(ArchNetAddress addr) return ntohs(ipAddr->sin6_port); } +#if HAVE_BLUETOOTH + case kBLUETOOTH: { + struct sockaddr_rc* ipAddr = + reinterpret_cast(&addr->m_addr); + return ipAddr->rc_channel; + } +#endif + default: assert(0 && "unknown address family"); return 0; @@ -855,6 +1165,21 @@ ArchNetworkBSD::isAnyAddr(ArchNetAddress addr) addr->m_len == (socklen_t)sizeof(struct sockaddr_in6)); } +#if HAVE_BLUETOOTH + case kBLUETOOTH: { + struct sockaddr_rc* BAddr = + reinterpret_cast(&addr->m_addr); + char *a, *b; + int n; + a = (char*)&BAddr->rc_bdaddr; + b = (char*)(&bdaddr_any); + for(n=0;n<6;n++) { + if(a[n]!=b[n]) return false; + } + return (addr->m_len == sizeof(struct sockaddr_rc)); + } +#endif + default: assert(0 && "unknown address family"); return true; diff --git a/src/lib/arch/unix/ArchNetworkBSD.h b/src/lib/arch/unix/ArchNetworkBSD.h index 3f5679a3..c52c2834 100644 --- a/src/lib/arch/unix/ArchNetworkBSD.h +++ b/src/lib/arch/unix/ArchNetworkBSD.h @@ -27,10 +27,13 @@ #if HAVE_SYS_SOCKET_H # include #endif - #if !HAVE_SOCKLEN_T typedef int socklen_t; #endif +#if HAVE_BLUETOOTH +# include +# include +#endif // old systems may use char* for [gs]etsockopt()'s optval argument. // this should be void on modern systems but char is forwards @@ -44,6 +47,7 @@ class ArchSocketImpl { public: int m_fd; int m_refCount; + IArchNetwork::EAddressFamily m_family; }; class ArchNetAddressImpl { @@ -100,6 +104,16 @@ private: void throwError(int); void throwNameError(int); +#if HAVE_BLUETOOTH + sdp_session_t *register_service(uint8_t rfcomm_channel); + ArchNetAddress find_channel(ArchNetAddress addr); + ArchNetAddress find_service(ArchNetAddress addr); +#endif + private: ArchMutex m_mutex; + +#if HAVE_BLUETOOTH + sdp_session_t * sdp_session; +#endif }; From c7ab51ba99dc785aa0af4b2d6a8bc4b8fc9772d6 Mon Sep 17 00:00:00 2001 From: Alistair Buxton Date: Thu, 5 Apr 2018 19:47:59 +0100 Subject: [PATCH 2/3] More optional code. --- src/lib/arch/unix/ArchNetworkBSD.cpp | 36 ++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/lib/arch/unix/ArchNetworkBSD.cpp b/src/lib/arch/unix/ArchNetworkBSD.cpp index 20c8e39a..36f5d55a 100644 --- a/src/lib/arch/unix/ArchNetworkBSD.cpp +++ b/src/lib/arch/unix/ArchNetworkBSD.cpp @@ -445,6 +445,18 @@ ArchNetworkBSD::listenOnSocket(ArchSocket s) if (listen(s->m_fd, 3) == -1) { throwError(errno); } + +#if HAVE_BLUETOOTH + if(s->m_family == kBLUETOOTH) { + struct sockaddr Addr; + memset(&Addr, 0, sizeof(Addr)); + socklen_t size = sizeof(Addr); + getsockname(s->m_fd, &Addr, &size); + struct sockaddr_rc* BAddr = + reinterpret_cast(&Addr); + register_service((uint8_t)BAddr->rc_channel); + } +#endif } ArchSocket @@ -827,6 +839,9 @@ ArchNetworkBSD::setNoDelayOnSocket(ArchSocket s, bool noDelay) { assert(s != NULL); + if(s->m_family == kBLUETOOTH) + return true; + // get old state int oflag; socklen_t size = (socklen_t)sizeof(oflag); @@ -947,6 +962,27 @@ ArchNetworkBSD::nameToAddr(const std::string& name) struct addrinfo *p; int ret; +#if HAVE_BLUETOOTH + if (s->m_family == kBLUETOOTH) { + struct sockaddr_rc inaddr; + memset(&inaddr, 0, sizeof(inaddr)); + if(str2ba(name.c_str(), &inaddr.rc_bdaddr) != 0) { + ARCH->unlockMutex(m_mutex); + delete addr; + throwNameError(ret); + } + + addr->m_len = sizeof(struct sockaddr_rc); + inaddr.rc_family = AF_BLUETOOTH; + inaddr.rc_channel = (uint8_t) 0; + memcpy(&addr->m_addr, &inaddr, addr->m_len); + + ARCH->unlockMutex(m_mutex); + + return addr; + } +#endif + memset(&hints, 0, sizeof(hints)); hints.ai_family = AF_UNSPEC; From 42245f8613b1990aa96cc22ff3d9c1803890e0c1 Mon Sep 17 00:00:00 2001 From: Alistair Buxton Date: Thu, 5 Apr 2018 19:49:21 +0100 Subject: [PATCH 3/3] Remaining breaking changes. --- src/lib/barrier/ArgParser.cpp | 9 ++++++--- src/lib/ipc/IpcClient.cpp | 4 ++-- src/lib/ipc/IpcServer.cpp | 2 +- src/lib/net/NetworkAddress.cpp | 4 ++-- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/lib/barrier/ArgParser.cpp b/src/lib/barrier/ArgParser.cpp index 20e849cf..4ffc4cc8 100644 --- a/src/lib/barrier/ArgParser.cpp +++ b/src/lib/barrier/ArgParser.cpp @@ -117,9 +117,12 @@ ArgParser::parseClientArgs(ClientArgs& args, int argc, const char* const* argv) // exactly one non-option argument (server-address) if (i == argc) { - LOG((CLOG_PRINT "%s: a server address or name is required" BYE, - args.m_pname, args.m_pname)); - return false; + // breaking + args.m_barrierAddress = "00:00:00:00:00:00"; + + //LOG((CLOG_PRINT "%s: a server address or name is required" BYE, + // args.m_pname, args.m_pname)); + //return false; } if (checkUnexpectedArgs()) { diff --git a/src/lib/ipc/IpcClient.cpp b/src/lib/ipc/IpcClient.cpp index 4eeae5b8..b8723db1 100644 --- a/src/lib/ipc/IpcClient.cpp +++ b/src/lib/ipc/IpcClient.cpp @@ -28,7 +28,7 @@ IpcClient::IpcClient(IEventQueue* events, SocketMultiplexer* socketMultiplexer) : m_serverAddress(NetworkAddress(IPC_HOST, IPC_PORT)), - m_socket(events, socketMultiplexer, IArchNetwork::kINET), + m_socket(events, socketMultiplexer, IArchNetwork::kBLUETOOTH), m_server(nullptr), m_events(events) { @@ -37,7 +37,7 @@ IpcClient::IpcClient(IEventQueue* events, SocketMultiplexer* socketMultiplexer) IpcClient::IpcClient(IEventQueue* events, SocketMultiplexer* socketMultiplexer, int port) : m_serverAddress(NetworkAddress(IPC_HOST, port)), - m_socket(events, socketMultiplexer, IArchNetwork::kINET), + m_socket(events, socketMultiplexer, IArchNetwork::kBLUETOOTH), m_server(nullptr), m_events(events) { diff --git a/src/lib/ipc/IpcServer.cpp b/src/lib/ipc/IpcServer.cpp index e05a913f..bef1aec0 100644 --- a/src/lib/ipc/IpcServer.cpp +++ b/src/lib/ipc/IpcServer.cpp @@ -54,7 +54,7 @@ IpcServer::IpcServer(IEventQueue* events, SocketMultiplexer* socketMultiplexer, void IpcServer::init() { - m_socket = new TCPListenSocket(m_events, m_socketMultiplexer, IArchNetwork::kINET); + m_socket = new TCPListenSocket(m_events, m_socketMultiplexer, IArchNetwork::kBLUETOOTH); m_clientsMutex = ARCH->newMutex(); m_address.resolve(); diff --git a/src/lib/net/NetworkAddress.cpp b/src/lib/net/NetworkAddress.cpp index c395ab03..99ce10b1 100644 --- a/src/lib/net/NetworkAddress.cpp +++ b/src/lib/net/NetworkAddress.cpp @@ -45,7 +45,7 @@ NetworkAddress::NetworkAddress(int port) : m_port(port) { checkPort(); - m_address = ARCH->newAnyAddr(IArchNetwork::kINET); + m_address = ARCH->newAnyAddr(IArchNetwork::kBLUETOOTH); ARCH->setAddrPort(m_address, m_port); } @@ -145,7 +145,7 @@ NetworkAddress::resolve() // if hostname is empty then use wildcard address otherwise look // up the name. if (m_hostname.empty()) { - m_address = ARCH->newAnyAddr(IArchNetwork::kINET); + m_address = ARCH->newAnyAddr(IArchNetwork::kBLUETOOTH); } else { m_address = ARCH->nameToAddr(m_hostname);