| // Copyright 2017 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include <errno.h> |
| #include <fcntl.h> |
| #include <stdio.h> |
| #include <stdlib.h> |
| #include <string.h> |
| |
| #include <fbl/alloc_checker.h> |
| #include <fbl/array.h> |
| #include <fbl/auto_call.h> |
| #include <fbl/string.h> |
| #include <fbl/unique_fd.h> |
| #include <fuchsia/device/manager/c/fidl.h> |
| #include <fuchsia/hardware/input/c/fidl.h> |
| #include <hid-parser/parser.h> |
| #include <hid-parser/usages.h> |
| #include <lib/fdio/directory.h> |
| #include <lib/fdio/fd.h> |
| #include <lib/fdio/fdio.h> |
| #include <lib/fdio/watcher.h> |
| #include <lib/fzl/fdio.h> |
| #include <lib/zx/channel.h> |
| #include <zircon/processargs.h> |
| #include <zircon/status.h> |
| #include <zircon/syscalls.h> |
| #include <ddk/device.h> |
| |
| #include <utility> |
| |
| #define INPUT_PATH "/input" |
| |
| namespace { |
| |
| bool usage_eq(const hid::Usage& u1, const hid::Usage& u2) { |
| return u1.page == u2.page && u1.usage == u2.usage; |
| } |
| |
| // Search the report descriptor for a System Power Down input field within a |
| // Generic Desktop:System Control collection. |
| // |
| // This method assumes the HID descriptor does not contain more than one such field. |
| zx_status_t FindSystemPowerDown(const hid::DeviceDescriptor* desc, |
| uint8_t* report_id, size_t* bit_offset) { |
| |
| const hid::Usage system_control = { |
| .page = static_cast<uint16_t>(hid::usage::Page::kGenericDesktop), |
| .usage = static_cast<uint32_t>(hid::usage::GenericDesktop::kSystemControl), |
| }; |
| |
| const hid::Usage power_down = { |
| .page = static_cast<uint16_t>(hid::usage::Page::kGenericDesktop), |
| .usage = static_cast<uint32_t>(hid::usage::GenericDesktop::kSystemPowerDown), |
| }; |
| |
| // Search for the field |
| for (size_t rpt_idx = 0; rpt_idx < desc->rep_count; ++rpt_idx) { |
| const hid::ReportDescriptor& report = desc->report[rpt_idx]; |
| |
| for (size_t i = 0; i < report.input_count; ++i) { |
| const hid::ReportField& field = report.input_fields[i]; |
| |
| if (!usage_eq(field.attr.usage, power_down)) { |
| continue; |
| } |
| |
| const hid::Collection* collection = hid::GetAppCollection(&field); |
| if (!collection || !usage_eq(collection->usage, system_control)) { |
| continue; |
| } |
| *report_id = field.report_id; |
| *bit_offset = field.attr.offset; |
| return ZX_OK; |
| } |
| } |
| return ZX_ERR_NOT_FOUND; |
| } |
| |
| struct PowerButtonInfo { |
| fbl::unique_fd fd; |
| uint8_t report_id; |
| size_t bit_offset; |
| bool has_report_id_byte; |
| }; |
| |
| static zx_status_t InputDeviceAdded(int dirfd, int event, const char* name, void* cookie) { |
| if (event != WATCH_EVENT_ADD_FILE) { |
| return ZX_OK; |
| } |
| |
| fbl::unique_fd fd; |
| { |
| int raw_fd; |
| if ((raw_fd = openat(dirfd, name, O_RDWR)) < 0) { |
| return ZX_OK; |
| } |
| fd.reset(raw_fd); |
| } |
| fzl::FdioCaller caller(std::move(fd)); |
| |
| // Retrieve and parse the report descriptor |
| uint16_t desc_len = 0; |
| zx_status_t status = |
| fuchsia_hardware_input_DeviceGetReportDescSize(caller.borrow_channel(), &desc_len); |
| if (status != ZX_OK) { |
| return ZX_OK; |
| } |
| if (desc_len > fuchsia_hardware_input_MAX_DESC_LEN) { |
| return ZX_OK; |
| } |
| |
| fbl::AllocChecker ac; |
| fbl::Array<uint8_t> raw_desc(new (&ac) uint8_t[desc_len](), desc_len); |
| if (!ac.check()) { |
| return ZX_OK; |
| } |
| |
| size_t actual_size; |
| status = fuchsia_hardware_input_DeviceGetReportDesc(caller.borrow_channel(), raw_desc.get(), |
| raw_desc.size(), &actual_size); |
| if (status != ZX_OK || actual_size != raw_desc.size()) { |
| return ZX_OK; |
| } |
| |
| hid::DeviceDescriptor* desc; |
| if (hid::ParseReportDescriptor(raw_desc.get(), raw_desc.size(), &desc) != hid::kParseOk) { |
| return ZX_OK; |
| } |
| auto cleanup_desc = fbl::MakeAutoCall([desc]() { hid::FreeDeviceDescriptor(desc); }); |
| |
| uint8_t report_id; |
| size_t bit_offset; |
| status = FindSystemPowerDown(desc, &report_id, &bit_offset); |
| if (status != ZX_OK) { |
| return ZX_OK; |
| } |
| |
| auto info = reinterpret_cast<PowerButtonInfo*>(cookie); |
| info->fd = caller.release(); |
| info->report_id = report_id; |
| info->bit_offset = bit_offset; |
| info->has_report_id_byte = (desc->rep_count > 1 || desc->report[0].report_id != 0); |
| return ZX_ERR_STOP; |
| } |
| |
| zx_status_t send_poweroff() { |
| zx::channel channel_local, channel_remote; |
| zx_status_t status = zx::channel::create(0, &channel_local, &channel_remote); |
| if (status != ZX_OK) { |
| printf("failed to create channel: %d\n", status); |
| return ZX_ERR_INTERNAL; |
| } |
| |
| const char* service = "/svc/" fuchsia_device_manager_Administrator_Name; |
| status = fdio_service_connect(service, channel_remote.get()); |
| if (status != ZX_OK) { |
| fprintf(stderr, "failed to connect to service %s: %d\n", service, status); |
| return ZX_ERR_INTERNAL; |
| } |
| |
| zx_status_t call_status; |
| status = fuchsia_device_manager_AdministratorSuspend(channel_local.get(), |
| DEVICE_SUSPEND_FLAG_POWEROFF, |
| &call_status); |
| if (status != ZX_OK || call_status != ZX_OK) { |
| fprintf(stderr, "Call to %s failed: ret: %d remote: %d\n", service, status, call_status); |
| return status != ZX_OK ? status : call_status; |
| } |
| |
| return ZX_OK; |
| } |
| |
| } // namespace |
| |
| int main(int argc, char**argv) { |
| fbl::unique_fd dirfd; |
| { |
| int fd = open(INPUT_PATH, O_DIRECTORY | O_RDONLY); |
| if (fd < 0) { |
| printf("pwrbtn-monitor: Failed to open " INPUT_PATH ": %d\n", errno); |
| return 1; |
| } |
| dirfd.reset(fd); |
| } |
| |
| PowerButtonInfo info; |
| zx_status_t status = fdio_watch_directory(dirfd.get(), InputDeviceAdded, ZX_TIME_INFINITE, &info); |
| if (status != ZX_ERR_STOP) { |
| printf("pwrbtn-monitor: Failed to find power button device\n"); |
| return 1; |
| } |
| dirfd.reset(); |
| |
| fzl::FdioCaller caller(std::move(info.fd)); |
| uint16_t report_size = 0; |
| if (fuchsia_hardware_input_DeviceGetMaxInputReportSize(caller.borrow_channel(), &report_size) != |
| ZX_OK) { |
| printf("pwrbtn-monitor: Failed to to get max report size\n"); |
| return 1; |
| } |
| |
| // Double-check the size looks right |
| const size_t byte_index = info.has_report_id_byte + info.bit_offset / 8; |
| if (report_size <= byte_index) { |
| printf("pwrbtn-monitor: Suspicious looking max report size\n"); |
| return 1; |
| } |
| |
| fbl::AllocChecker ac; |
| fbl::Array<uint8_t> report(new (&ac) uint8_t[report_size](), report_size); |
| if (!ac.check()) { |
| return 1; |
| } |
| |
| info.fd = caller.release(); |
| |
| // Watch the power button device for reports |
| while (true) { |
| ssize_t r = read(info.fd.get(), report.get(), report.size()); |
| if (r < 0) { |
| printf("pwrbtn-monitor: got read error %zd, bailing\n", r); |
| return 1; |
| } |
| |
| // Ignore reports from different report IDs |
| if (info.has_report_id_byte && report[0] != info.report_id) { |
| printf("pwrbtn-monitor: input-watcher: wrong id\n"); |
| continue; |
| } |
| |
| if (static_cast<size_t>(r) <= byte_index) { |
| printf("pwrbtn-monitor: input-watcher: too short\n"); |
| continue; |
| } |
| |
| // Check if the power button is pressed, and request a poweroff if so. |
| if (report[byte_index] & (1u << (info.bit_offset % 8))) { |
| auto status = send_poweroff(); |
| if (status != ZX_OK) { |
| printf("pwrbtn-monitor: input-watcher: failed send poweroff to device manager.\n"); |
| continue; |
| } |
| } |
| } |
| } |