ipc_handler_linux.cc revision e415c050edbb2710e8807dd2602c851412953268
1fe65fb7978bc9257a36d1e5eae59c5f412dbdb49Arman Uguray// 2fe65fb7978bc9257a36d1e5eae59c5f412dbdb49Arman Uguray// Copyright (C) 2015 Google, Inc. 3fe65fb7978bc9257a36d1e5eae59c5f412dbdb49Arman Uguray// 4fe65fb7978bc9257a36d1e5eae59c5f412dbdb49Arman Uguray// Licensed under the Apache License, Version 2.0 (the "License"); 5fe65fb7978bc9257a36d1e5eae59c5f412dbdb49Arman Uguray// you may not use this file except in compliance with the License. 6fe65fb7978bc9257a36d1e5eae59c5f412dbdb49Arman Uguray// You may obtain a copy of the License at: 7fe65fb7978bc9257a36d1e5eae59c5f412dbdb49Arman Uguray// 8fe65fb7978bc9257a36d1e5eae59c5f412dbdb49Arman Uguray// http://www.apache.org/licenses/LICENSE-2.0 9fe65fb7978bc9257a36d1e5eae59c5f412dbdb49Arman Uguray// 10fe65fb7978bc9257a36d1e5eae59c5f412dbdb49Arman Uguray// Unless required by applicable law or agreed to in writing, software 11fe65fb7978bc9257a36d1e5eae59c5f412dbdb49Arman Uguray// distributed under the License is distributed on an "AS IS" BASIS, 12fe65fb7978bc9257a36d1e5eae59c5f412dbdb49Arman Uguray// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13fe65fb7978bc9257a36d1e5eae59c5f412dbdb49Arman Uguray// See the License for the specific language governing permissions and 14fe65fb7978bc9257a36d1e5eae59c5f412dbdb49Arman Uguray// limitations under the License. 15fe65fb7978bc9257a36d1e5eae59c5f412dbdb49Arman Uguray// 16fe65fb7978bc9257a36d1e5eae59c5f412dbdb49Arman Uguray 17e415c050edbb2710e8807dd2602c851412953268Scott James Remnant#include "service/ipc/ipc_handler_linux.h" 18fe65fb7978bc9257a36d1e5eae59c5f412dbdb49Arman Uguray 19fe65fb7978bc9257a36d1e5eae59c5f412dbdb49Arman Uguray#include <sys/socket.h> 20fe65fb7978bc9257a36d1e5eae59c5f412dbdb49Arman Uguray#include <sys/un.h> 21fe65fb7978bc9257a36d1e5eae59c5f412dbdb49Arman Uguray 22fe65fb7978bc9257a36d1e5eae59c5f412dbdb49Arman Uguray#include <base/bind.h> 23fe65fb7978bc9257a36d1e5eae59c5f412dbdb49Arman Uguray 24c6760d82357f46943406c579f5b1c291a20afdebIan Coolidge#include "osi/include/socket_utils/sockets.h" 25fe65fb7978bc9257a36d1e5eae59c5f412dbdb49Arman Uguray#include "service/daemon.h" 26e415c050edbb2710e8807dd2602c851412953268Scott James Remnant#include "service/ipc/linux_ipc_host.h" 27fe65fb7978bc9257a36d1e5eae59c5f412dbdb49Arman Uguray#include "service/settings.h" 28fe65fb7978bc9257a36d1e5eae59c5f412dbdb49Arman Uguray 29fe65fb7978bc9257a36d1e5eae59c5f412dbdb49Arman Uguraynamespace ipc { 30fe65fb7978bc9257a36d1e5eae59c5f412dbdb49Arman Uguray 31e415c050edbb2710e8807dd2602c851412953268Scott James RemnantIPCHandlerLinux::IPCHandlerLinux(bluetooth::Adapter* adapter, 32e0d08c9fecdf92c386b52375501a306c8f67d63eArman Uguray IPCManager::Delegate* delegate) 33d6a4b0c950f44d3eab34825880d26c19e362d22bArman Uguray : IPCHandler(adapter, delegate), 34fe65fb7978bc9257a36d1e5eae59c5f412dbdb49Arman Uguray running_(false), 35e415c050edbb2710e8807dd2602c851412953268Scott James Remnant thread_("IPCHandlerLinux"), 36e0d08c9fecdf92c386b52375501a306c8f67d63eArman Uguray keep_running_(true) { 37fe65fb7978bc9257a36d1e5eae59c5f412dbdb49Arman Uguray} 38fe65fb7978bc9257a36d1e5eae59c5f412dbdb49Arman Uguray 39e415c050edbb2710e8807dd2602c851412953268Scott James RemnantIPCHandlerLinux::~IPCHandlerLinux() { 40e0d08c9fecdf92c386b52375501a306c8f67d63eArman Uguray // This will only be set if the Settings::create_ipc_socket_path() was 41e0d08c9fecdf92c386b52375501a306c8f67d63eArman Uguray // originally provided. 42e0d08c9fecdf92c386b52375501a306c8f67d63eArman Uguray if (!socket_path_.empty()) 43e0d08c9fecdf92c386b52375501a306c8f67d63eArman Uguray unlink(socket_path_.value().c_str()); 44fe65fb7978bc9257a36d1e5eae59c5f412dbdb49Arman Uguray} 45fe65fb7978bc9257a36d1e5eae59c5f412dbdb49Arman Uguray 46e415c050edbb2710e8807dd2602c851412953268Scott James Remnantbool IPCHandlerLinux::Run() { 47fe65fb7978bc9257a36d1e5eae59c5f412dbdb49Arman Uguray CHECK(!running_); 48fe65fb7978bc9257a36d1e5eae59c5f412dbdb49Arman Uguray 49c6760d82357f46943406c579f5b1c291a20afdebIan Coolidge const std::string& android_suffix = 50f8881fee3d08cb50896b22adc0841223694d51d2Arman Uguray bluetooth::Daemon::Get()->GetSettings()->android_ipc_socket_suffix(); 51fe65fb7978bc9257a36d1e5eae59c5f412dbdb49Arman Uguray const base::FilePath& path = 52f8881fee3d08cb50896b22adc0841223694d51d2Arman Uguray bluetooth::Daemon::Get()->GetSettings()->create_ipc_socket_path(); 53c6760d82357f46943406c579f5b1c291a20afdebIan Coolidge 54c6760d82357f46943406c579f5b1c291a20afdebIan Coolidge // Both flags cannot be set at the same time. 55c6760d82357f46943406c579f5b1c291a20afdebIan Coolidge CHECK(android_suffix.empty() || path.empty()); 56c6760d82357f46943406c579f5b1c291a20afdebIan Coolidge if (android_suffix.empty() && path.empty()) { 57fe65fb7978bc9257a36d1e5eae59c5f412dbdb49Arman Uguray LOG(ERROR) << "No domain socket path provided"; 58fe65fb7978bc9257a36d1e5eae59c5f412dbdb49Arman Uguray return false; 59fe65fb7978bc9257a36d1e5eae59c5f412dbdb49Arman Uguray } 60fe65fb7978bc9257a36d1e5eae59c5f412dbdb49Arman Uguray 61fe65fb7978bc9257a36d1e5eae59c5f412dbdb49Arman Uguray CHECK(base::MessageLoop::current()); // An origin event loop is required. 62fe65fb7978bc9257a36d1e5eae59c5f412dbdb49Arman Uguray origin_task_runner_ = base::MessageLoop::current()->task_runner(); 63fe65fb7978bc9257a36d1e5eae59c5f412dbdb49Arman Uguray 64c6760d82357f46943406c579f5b1c291a20afdebIan Coolidge if (!android_suffix.empty()) { 65c6760d82357f46943406c579f5b1c291a20afdebIan Coolidge int server_fd = osi_android_get_control_socket(android_suffix.c_str()); 66c6760d82357f46943406c579f5b1c291a20afdebIan Coolidge if (server_fd == -1) { 67c6760d82357f46943406c579f5b1c291a20afdebIan Coolidge LOG(ERROR) << "Unable to get Android socket from: " << android_suffix; 68c6760d82357f46943406c579f5b1c291a20afdebIan Coolidge return false; 69c6760d82357f46943406c579f5b1c291a20afdebIan Coolidge } 70c6760d82357f46943406c579f5b1c291a20afdebIan Coolidge LOG(INFO) << "Binding to Android server socket:" << android_suffix; 71c6760d82357f46943406c579f5b1c291a20afdebIan Coolidge socket_.reset(server_fd); 72c6760d82357f46943406c579f5b1c291a20afdebIan Coolidge } else { 73c6760d82357f46943406c579f5b1c291a20afdebIan Coolidge LOG(INFO) << "Creating a Unix domain socket:" << path.value(); 74c6760d82357f46943406c579f5b1c291a20afdebIan Coolidge 75c6760d82357f46943406c579f5b1c291a20afdebIan Coolidge // TODO(armansito): This is opens the door to potentially unlinking files in 76c6760d82357f46943406c579f5b1c291a20afdebIan Coolidge // the current directory that we're not supposed to. For now we will have an 77c6760d82357f46943406c579f5b1c291a20afdebIan Coolidge // assumption that the daemon runs in a sandbox but we should generally do 78c6760d82357f46943406c579f5b1c291a20afdebIan Coolidge // this properly. 79c6760d82357f46943406c579f5b1c291a20afdebIan Coolidge unlink(path.value().c_str()); 80c6760d82357f46943406c579f5b1c291a20afdebIan Coolidge 81c6760d82357f46943406c579f5b1c291a20afdebIan Coolidge base::ScopedFD server_socket(socket(PF_UNIX, SOCK_SEQPACKET, 0)); 82c6760d82357f46943406c579f5b1c291a20afdebIan Coolidge if (!server_socket.is_valid()) { 83c6760d82357f46943406c579f5b1c291a20afdebIan Coolidge LOG(ERROR) << "Failed to open domain socket for IPC"; 84c6760d82357f46943406c579f5b1c291a20afdebIan Coolidge return false; 85c6760d82357f46943406c579f5b1c291a20afdebIan Coolidge } 86c6760d82357f46943406c579f5b1c291a20afdebIan Coolidge 87c6760d82357f46943406c579f5b1c291a20afdebIan Coolidge struct sockaddr_un address; 88c6760d82357f46943406c579f5b1c291a20afdebIan Coolidge memset(&address, 0, sizeof(address)); 89c6760d82357f46943406c579f5b1c291a20afdebIan Coolidge address.sun_family = AF_UNIX; 90c6760d82357f46943406c579f5b1c291a20afdebIan Coolidge strncpy(address.sun_path, path.value().c_str(), 91c6760d82357f46943406c579f5b1c291a20afdebIan Coolidge sizeof(address.sun_path) - 1); 92c6760d82357f46943406c579f5b1c291a20afdebIan Coolidge if (bind(server_socket.get(), (struct sockaddr*)&address, sizeof(address)) < 93c6760d82357f46943406c579f5b1c291a20afdebIan Coolidge 0) { 94c6760d82357f46943406c579f5b1c291a20afdebIan Coolidge LOG(ERROR) << "Failed to bind IPC socket to address: " << strerror(errno); 95c6760d82357f46943406c579f5b1c291a20afdebIan Coolidge return false; 96c6760d82357f46943406c579f5b1c291a20afdebIan Coolidge } 97c6760d82357f46943406c579f5b1c291a20afdebIan Coolidge 98c6760d82357f46943406c579f5b1c291a20afdebIan Coolidge socket_.swap(server_socket); 99e0d08c9fecdf92c386b52375501a306c8f67d63eArman Uguray socket_path_ = path; 100fe65fb7978bc9257a36d1e5eae59c5f412dbdb49Arman Uguray } 101fe65fb7978bc9257a36d1e5eae59c5f412dbdb49Arman Uguray 102c6760d82357f46943406c579f5b1c291a20afdebIan Coolidge CHECK(socket_.is_valid()); 103fe65fb7978bc9257a36d1e5eae59c5f412dbdb49Arman Uguray 104fe65fb7978bc9257a36d1e5eae59c5f412dbdb49Arman Uguray running_ = true; // Set this here before launching the thread. 105fe65fb7978bc9257a36d1e5eae59c5f412dbdb49Arman Uguray 106fe65fb7978bc9257a36d1e5eae59c5f412dbdb49Arman Uguray // Start an IO thread and post the listening task. 107fe65fb7978bc9257a36d1e5eae59c5f412dbdb49Arman Uguray base::Thread::Options options(base::MessageLoop::TYPE_IO, 0); 108fe65fb7978bc9257a36d1e5eae59c5f412dbdb49Arman Uguray if (!thread_.StartWithOptions(options)) { 109e415c050edbb2710e8807dd2602c851412953268Scott James Remnant LOG(ERROR) << "Failed to start IPCHandlerLinux thread"; 110fe65fb7978bc9257a36d1e5eae59c5f412dbdb49Arman Uguray running_ = false; 111fe65fb7978bc9257a36d1e5eae59c5f412dbdb49Arman Uguray return false; 112fe65fb7978bc9257a36d1e5eae59c5f412dbdb49Arman Uguray } 113fe65fb7978bc9257a36d1e5eae59c5f412dbdb49Arman Uguray 114fe65fb7978bc9257a36d1e5eae59c5f412dbdb49Arman Uguray thread_.task_runner()->PostTask( 115fe65fb7978bc9257a36d1e5eae59c5f412dbdb49Arman Uguray FROM_HERE, 116e415c050edbb2710e8807dd2602c851412953268Scott James Remnant base::Bind(&IPCHandlerLinux::StartListeningOnThread, this)); 117fe65fb7978bc9257a36d1e5eae59c5f412dbdb49Arman Uguray 118fe65fb7978bc9257a36d1e5eae59c5f412dbdb49Arman Uguray return true; 119fe65fb7978bc9257a36d1e5eae59c5f412dbdb49Arman Uguray} 120fe65fb7978bc9257a36d1e5eae59c5f412dbdb49Arman Uguray 121e415c050edbb2710e8807dd2602c851412953268Scott James Remnantvoid IPCHandlerLinux::Stop() { 122e0d08c9fecdf92c386b52375501a306c8f67d63eArman Uguray keep_running_ = false; 123e0d08c9fecdf92c386b52375501a306c8f67d63eArman Uguray 124e0d08c9fecdf92c386b52375501a306c8f67d63eArman Uguray // At this moment the listening thread might be blocking on the accept 125e0d08c9fecdf92c386b52375501a306c8f67d63eArman Uguray // syscall. Shutdown and close the server socket before joining the thread to 126e0d08c9fecdf92c386b52375501a306c8f67d63eArman Uguray // interrupt accept so that the main thread doesn't keep blocking. 127e0d08c9fecdf92c386b52375501a306c8f67d63eArman Uguray shutdown(socket_.get(), SHUT_RDWR); 128e0d08c9fecdf92c386b52375501a306c8f67d63eArman Uguray socket_.reset(); 129e0d08c9fecdf92c386b52375501a306c8f67d63eArman Uguray 130e0d08c9fecdf92c386b52375501a306c8f67d63eArman Uguray // Join and clean up the thread. 131e0d08c9fecdf92c386b52375501a306c8f67d63eArman Uguray thread_.Stop(); 132e0d08c9fecdf92c386b52375501a306c8f67d63eArman Uguray 133e0d08c9fecdf92c386b52375501a306c8f67d63eArman Uguray // Thread exited. Notify the delegate. Post this on the event loop so that the 134e0d08c9fecdf92c386b52375501a306c8f67d63eArman Uguray // callback isn't reentrant. 135e0d08c9fecdf92c386b52375501a306c8f67d63eArman Uguray NotifyStoppedOnOriginThread(); 136e0d08c9fecdf92c386b52375501a306c8f67d63eArman Uguray} 137e0d08c9fecdf92c386b52375501a306c8f67d63eArman Uguray 138e415c050edbb2710e8807dd2602c851412953268Scott James Remnantvoid IPCHandlerLinux::StartListeningOnThread() { 139fe65fb7978bc9257a36d1e5eae59c5f412dbdb49Arman Uguray CHECK(socket_.is_valid()); 140d6a4b0c950f44d3eab34825880d26c19e362d22bArman Uguray CHECK(adapter()); 141fe65fb7978bc9257a36d1e5eae59c5f412dbdb49Arman Uguray CHECK(running_); 142fe65fb7978bc9257a36d1e5eae59c5f412dbdb49Arman Uguray 143fe65fb7978bc9257a36d1e5eae59c5f412dbdb49Arman Uguray LOG(INFO) << "Listening to incoming connections"; 144fe65fb7978bc9257a36d1e5eae59c5f412dbdb49Arman Uguray 145fe65fb7978bc9257a36d1e5eae59c5f412dbdb49Arman Uguray int status = listen(socket_.get(), SOMAXCONN); 146fe65fb7978bc9257a36d1e5eae59c5f412dbdb49Arman Uguray if (status < 0) { 147fe65fb7978bc9257a36d1e5eae59c5f412dbdb49Arman Uguray LOG(ERROR) << "Failed to listen on domain socket: " << strerror(errno); 148fe65fb7978bc9257a36d1e5eae59c5f412dbdb49Arman Uguray origin_task_runner_->PostTask( 149fe65fb7978bc9257a36d1e5eae59c5f412dbdb49Arman Uguray FROM_HERE, 150e415c050edbb2710e8807dd2602c851412953268Scott James Remnant base::Bind(&IPCHandlerLinux::ShutDownOnOriginThread, this)); 151fe65fb7978bc9257a36d1e5eae59c5f412dbdb49Arman Uguray return; 152fe65fb7978bc9257a36d1e5eae59c5f412dbdb49Arman Uguray } 153fe65fb7978bc9257a36d1e5eae59c5f412dbdb49Arman Uguray 154e0d08c9fecdf92c386b52375501a306c8f67d63eArman Uguray NotifyStartedOnOriginThread(); 155e0d08c9fecdf92c386b52375501a306c8f67d63eArman Uguray 156e0d08c9fecdf92c386b52375501a306c8f67d63eArman Uguray // TODO(armansito): The code below can cause the daemon to run indefinitely if 157e0d08c9fecdf92c386b52375501a306c8f67d63eArman Uguray // the thread is joined while it's in the middle of the EventLoop() call. The 158e0d08c9fecdf92c386b52375501a306c8f67d63eArman Uguray // EventLoop() won't exit until a client terminates the connection, however 159e0d08c9fecdf92c386b52375501a306c8f67d63eArman Uguray // this can be fixed by using the |thread_|'s MessageLoopForIO instead (since 160e0d08c9fecdf92c386b52375501a306c8f67d63eArman Uguray // it gets stopped along with the thread). 161fe65fb7978bc9257a36d1e5eae59c5f412dbdb49Arman Uguray // TODO(icoolidge): accept simultaneous clients 162e0d08c9fecdf92c386b52375501a306c8f67d63eArman Uguray while (keep_running_.load()) { 163fe65fb7978bc9257a36d1e5eae59c5f412dbdb49Arman Uguray int client_socket = accept4(socket_.get(), nullptr, nullptr, SOCK_NONBLOCK); 164e0d08c9fecdf92c386b52375501a306c8f67d63eArman Uguray if (client_socket < 0) { 165fe65fb7978bc9257a36d1e5eae59c5f412dbdb49Arman Uguray LOG(ERROR) << "Failed to accept client connection: " << strerror(errno); 166fe65fb7978bc9257a36d1e5eae59c5f412dbdb49Arman Uguray continue; 167fe65fb7978bc9257a36d1e5eae59c5f412dbdb49Arman Uguray } 168fe65fb7978bc9257a36d1e5eae59c5f412dbdb49Arman Uguray 169fe65fb7978bc9257a36d1e5eae59c5f412dbdb49Arman Uguray LOG(INFO) << "Established client connection: fd=" << client_socket; 170d6a4b0c950f44d3eab34825880d26c19e362d22bArman Uguray 171e415c050edbb2710e8807dd2602c851412953268Scott James Remnant LinuxIPCHost ipc_host(client_socket, adapter()); 172d6a4b0c950f44d3eab34825880d26c19e362d22bArman Uguray 173fe65fb7978bc9257a36d1e5eae59c5f412dbdb49Arman Uguray // TODO(armansito): Use |thread_|'s MessageLoopForIO instead of using a 174fe65fb7978bc9257a36d1e5eae59c5f412dbdb49Arman Uguray // custom event loop to poll from the socket. 175fe65fb7978bc9257a36d1e5eae59c5f412dbdb49Arman Uguray ipc_host.EventLoop(); 176fe65fb7978bc9257a36d1e5eae59c5f412dbdb49Arman Uguray } 177fe65fb7978bc9257a36d1e5eae59c5f412dbdb49Arman Uguray} 178fe65fb7978bc9257a36d1e5eae59c5f412dbdb49Arman Uguray 179e415c050edbb2710e8807dd2602c851412953268Scott James Remnantvoid IPCHandlerLinux::ShutDownOnOriginThread() { 180e415c050edbb2710e8807dd2602c851412953268Scott James Remnant LOG(INFO) << "Shutting down IPCHandlerLinux thread"; 181fe65fb7978bc9257a36d1e5eae59c5f412dbdb49Arman Uguray thread_.Stop(); 182fe65fb7978bc9257a36d1e5eae59c5f412dbdb49Arman Uguray running_ = false; 183fe65fb7978bc9257a36d1e5eae59c5f412dbdb49Arman Uguray 184e0d08c9fecdf92c386b52375501a306c8f67d63eArman Uguray NotifyStoppedOnCurrentThread(); 185e0d08c9fecdf92c386b52375501a306c8f67d63eArman Uguray} 186e0d08c9fecdf92c386b52375501a306c8f67d63eArman Uguray 187e415c050edbb2710e8807dd2602c851412953268Scott James Remnantvoid IPCHandlerLinux::NotifyStartedOnOriginThread() { 188e0d08c9fecdf92c386b52375501a306c8f67d63eArman Uguray if (!delegate()) 189e0d08c9fecdf92c386b52375501a306c8f67d63eArman Uguray return; 190e0d08c9fecdf92c386b52375501a306c8f67d63eArman Uguray 191e0d08c9fecdf92c386b52375501a306c8f67d63eArman Uguray origin_task_runner_->PostTask( 192e0d08c9fecdf92c386b52375501a306c8f67d63eArman Uguray FROM_HERE, 193e415c050edbb2710e8807dd2602c851412953268Scott James Remnant base::Bind(&IPCHandlerLinux::NotifyStartedOnCurrentThread, this)); 194e0d08c9fecdf92c386b52375501a306c8f67d63eArman Uguray} 195e0d08c9fecdf92c386b52375501a306c8f67d63eArman Uguray 196e415c050edbb2710e8807dd2602c851412953268Scott James Remnantvoid IPCHandlerLinux::NotifyStartedOnCurrentThread() { 197e0d08c9fecdf92c386b52375501a306c8f67d63eArman Uguray if (delegate()) 198e415c050edbb2710e8807dd2602c851412953268Scott James Remnant delegate()->OnIPCHandlerStarted(IPCManager::TYPE_LINUX); 199e0d08c9fecdf92c386b52375501a306c8f67d63eArman Uguray} 200e0d08c9fecdf92c386b52375501a306c8f67d63eArman Uguray 201e415c050edbb2710e8807dd2602c851412953268Scott James Remnantvoid IPCHandlerLinux::NotifyStoppedOnOriginThread() { 202e0d08c9fecdf92c386b52375501a306c8f67d63eArman Uguray if (!delegate()) 203e0d08c9fecdf92c386b52375501a306c8f67d63eArman Uguray return; 204e0d08c9fecdf92c386b52375501a306c8f67d63eArman Uguray 205e0d08c9fecdf92c386b52375501a306c8f67d63eArman Uguray origin_task_runner_->PostTask( 206e0d08c9fecdf92c386b52375501a306c8f67d63eArman Uguray FROM_HERE, 207e415c050edbb2710e8807dd2602c851412953268Scott James Remnant base::Bind(&IPCHandlerLinux::NotifyStoppedOnCurrentThread, this)); 208e0d08c9fecdf92c386b52375501a306c8f67d63eArman Uguray} 209e0d08c9fecdf92c386b52375501a306c8f67d63eArman Uguray 210e415c050edbb2710e8807dd2602c851412953268Scott James Remnantvoid IPCHandlerLinux::NotifyStoppedOnCurrentThread() { 211e0d08c9fecdf92c386b52375501a306c8f67d63eArman Uguray if (delegate()) 212e415c050edbb2710e8807dd2602c851412953268Scott James Remnant delegate()->OnIPCHandlerStopped(IPCManager::TYPE_LINUX); 213fe65fb7978bc9257a36d1e5eae59c5f412dbdb49Arman Uguray} 214fe65fb7978bc9257a36d1e5eae59c5f412dbdb49Arman Uguray 215fe65fb7978bc9257a36d1e5eae59c5f412dbdb49Arman Uguray} // namespace ipc 216