/* * cros_ec_debugfs - debug logs for Chrome OS EC * * Copyright 2015 Google, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #include #include #include #include #include #include #include #include #include #include #include "cros_ec_dev.h" #include "cros_ec_debugfs.h" #define LOG_SHIFT 14 #define LOG_SIZE (1 << LOG_SHIFT) #define LOG_POLL_SEC 10 #define CIRC_ADD(idx, size, value) (((idx) + (value)) & ((size) - 1)) /* struct cros_ec_debugfs - ChromeOS EC debugging information * * @ec: EC device this debugfs information belongs to * @dir: dentry for debugfs files * @log_buffer: circular buffer for console log information * @read_msg: preallocated EC command and buffer to read console log * @log_mutex: mutex to protect circular buffer * @log_wq: waitqueue for log readers * @log_poll_work: recurring task to poll EC for new console log data * @panicinfo_blob: panicinfo debugfs blob */ struct cros_ec_debugfs { struct cros_ec_dev *ec; struct dentry *dir; /* EC log */ struct circ_buf log_buffer; struct cros_ec_command *read_msg; struct mutex log_mutex; wait_queue_head_t log_wq; struct delayed_work log_poll_work; /* EC panicinfo */ struct debugfs_blob_wrapper panicinfo_blob; }; /* * We need to make sure that the EC log buffer on the UART is large enough, * so that it is unlikely enough to overlow within LOG_POLL_SEC. */ static void cros_ec_console_log_work(struct work_struct *__work) { struct cros_ec_debugfs *debug_info = container_of(to_delayed_work(__work), struct cros_ec_debugfs, log_poll_work); struct cros_ec_dev *ec = debug_info->ec; struct circ_buf *cb = &debug_info->log_buffer; struct cros_ec_command snapshot_msg = { .command = EC_CMD_CONSOLE_SNAPSHOT + ec->cmd_offset, }; struct ec_params_console_read_v1 *read_params = (struct ec_params_console_read_v1 *)debug_info->read_msg->data; uint8_t *ec_buffer = (uint8_t *)debug_info->read_msg->data; int idx; int buf_space; int ret; ret = cros_ec_cmd_xfer(ec->ec_dev, &snapshot_msg); if (ret < 0) { dev_err(ec->dev, "EC communication failed\n"); goto resched; } if (snapshot_msg.result != EC_RES_SUCCESS) { dev_err(ec->dev, "EC failed to snapshot the console log\n"); goto resched; } /* Loop until we have read everything, or there's an error. */ mutex_lock(&debug_info->log_mutex); buf_space = CIRC_SPACE(cb->head, cb->tail, LOG_SIZE); while (1) { if (!buf_space) { dev_info_once(ec->dev, "Some logs may have been dropped...\n"); break; } memset(read_params, '\0', sizeof(*read_params)); read_params->subcmd = CONSOLE_READ_RECENT; ret = cros_ec_cmd_xfer(ec->ec_dev, debug_info->read_msg); if (ret < 0) { dev_err(ec->dev, "EC communication failed\n"); break; } if (debug_info->read_msg->result != EC_RES_SUCCESS) { dev_err(ec->dev, "EC failed to read the console log\n"); break; } /* If the buffer is empty, we're done here. */ if (ret == 0 || ec_buffer[0] == '\0') break; idx = 0; while (idx < ret && ec_buffer[idx] != '\0' && buf_space > 0) { cb->buf[cb->head] = ec_buffer[idx]; cb->head = CIRC_ADD(cb->head, LOG_SIZE, 1); idx++; buf_space--; } wake_up(&debug_info->log_wq); } mutex_unlock(&debug_info->log_mutex); resched: schedule_delayed_work(&debug_info->log_poll_work, msecs_to_jiffies(LOG_POLL_SEC * 1000)); } static int cros_ec_console_log_open(struct inode *inode, struct file *file) { file->private_data = inode->i_private; return nonseekable_open(inode, file); } static ssize_t cros_ec_console_log_read(struct file *file, char __user *buf, size_t count, loff_t *ppos) { struct cros_ec_debugfs *debug_info = file->private_data; struct circ_buf *cb = &debug_info->log_buffer; ssize_t ret; mutex_lock(&debug_info->log_mutex); while (!CIRC_CNT(cb->head, cb->tail, LOG_SIZE)) { if (file->f_flags & O_NONBLOCK) { ret = -EAGAIN; goto error; } mutex_unlock(&debug_info->log_mutex); ret = wait_event_interruptible(debug_info->log_wq, CIRC_CNT(cb->head, cb->tail, LOG_SIZE)); if (ret < 0) return ret; mutex_lock(&debug_info->log_mutex); } /* Only copy until the end of the circular buffer, and let userspace * retry to get the rest of the data. */ ret = min_t(size_t, CIRC_CNT_TO_END(cb->head, cb->tail, LOG_SIZE), count); if (copy_to_user(buf, cb->buf + cb->tail, ret)) { ret = -EFAULT; goto error; } cb->tail = CIRC_ADD(cb->tail, LOG_SIZE, ret); error: mutex_unlock(&debug_info->log_mutex); return ret; } static __poll_t cros_ec_console_log_poll(struct file *file, poll_table *wait) { struct cros_ec_debugfs *debug_info = file->private_data; __poll_t mask = 0; poll_wait(file, &debug_info->log_wq, wait); mutex_lock(&debug_info->log_mutex); if (CIRC_CNT(debug_info->log_buffer.head, debug_info->log_buffer.tail, LOG_SIZE)) mask |= POLLIN | POLLRDNORM; mutex_unlock(&debug_info->log_mutex); return mask; } static int cros_ec_console_log_release(struct inode *inode, struct file *file) { return 0; } const struct file_operations cros_ec_console_log_fops = { .owner = THIS_MODULE, .open = cros_ec_console_log_open, .read = cros_ec_console_log_read, .llseek = no_llseek, .poll = cros_ec_console_log_poll, .release = cros_ec_console_log_release, }; static int ec_read_version_supported(struct cros_ec_dev *ec) { struct ec_params_get_cmd_versions_v1 *params; struct ec_response_get_cmd_versions *response; int ret; struct cros_ec_command *msg; msg = kzalloc(sizeof(*msg) + max(sizeof(*params), sizeof(*response)), GFP_KERNEL); if (!msg) return 0; msg->command = EC_CMD_GET_CMD_VERSIONS + ec->cmd_offset; msg->outsize = sizeof(*params); msg->insize = sizeof(*response); params = (struct ec_params_get_cmd_versions_v1 *)msg->data; params->cmd = EC_CMD_CONSOLE_READ; response = (struct ec_response_get_cmd_versions *)msg->data; ret = cros_ec_cmd_xfer(ec->ec_dev, msg) >= 0 && msg->result == EC_RES_SUCCESS && (response->version_mask & EC_VER_MASK(1)); kfree(msg); return ret; } static int cros_ec_create_console_log(struct cros_ec_debugfs *debug_info) { struct cros_ec_dev *ec = debug_info->ec; char *buf; int read_params_size; int read_response_size; if (!ec_read_version_supported(ec)) { dev_warn(ec->dev, "device does not support reading the console log\n"); return 0; } buf = devm_kzalloc(ec->dev, LOG_SIZE, GFP_KERNEL); if (!buf) return -ENOMEM; read_params_size = sizeof(struct ec_params_console_read_v1); read_response_size = ec->ec_dev->max_response; debug_info->read_msg = devm_kzalloc(ec->dev, sizeof(*debug_info->read_msg) + max(read_params_size, read_response_size), GFP_KERNEL); if (!debug_info->read_msg) return -ENOMEM; debug_info->read_msg->version = 1; debug_info->read_msg->command = EC_CMD_CONSOLE_READ + ec->cmd_offset; debug_info->read_msg->outsize = read_params_size; debug_info->read_msg->insize = read_response_size; debug_info->log_buffer.buf = buf; debug_info->log_buffer.head = 0; debug_info->log_buffer.tail = 0; mutex_init(&debug_info->log_mutex); init_waitqueue_head(&debug_info->log_wq); if (!debugfs_create_file("console_log", S_IFREG | S_IRUGO, debug_info->dir, debug_info, &cros_ec_console_log_fops)) return -ENOMEM; INIT_DELAYED_WORK(&debug_info->log_poll_work, cros_ec_console_log_work); schedule_delayed_work(&debug_info->log_poll_work, 0); return 0; } static void cros_ec_cleanup_console_log(struct cros_ec_debugfs *debug_info) { if (debug_info->log_buffer.buf) { cancel_delayed_work_sync(&debug_info->log_poll_work); mutex_destroy(&debug_info->log_mutex); } } static int cros_ec_create_panicinfo(struct cros_ec_debugfs *debug_info) { struct cros_ec_device *ec_dev = debug_info->ec->ec_dev; int ret; struct cros_ec_command *msg; int insize; insize = ec_dev->max_response; msg = devm_kzalloc(debug_info->ec->dev, sizeof(*msg) + insize, GFP_KERNEL); if (!msg) return -ENOMEM; msg->command = EC_CMD_GET_PANIC_INFO; msg->insize = insize; ret = cros_ec_cmd_xfer(ec_dev, msg); if (ret < 0) { dev_warn(debug_info->ec->dev, "Cannot read panicinfo.\n"); ret = 0; goto free; } /* No panic data */ if (ret == 0) goto free; debug_info->panicinfo_blob.data = msg->data; debug_info->panicinfo_blob.size = ret; if (!debugfs_create_blob("panicinfo", S_IFREG | S_IRUGO, debug_info->dir, &debug_info->panicinfo_blob)) { ret = -ENOMEM; goto free; } return 0; free: devm_kfree(debug_info->ec->dev, msg); return ret; } int cros_ec_debugfs_init(struct cros_ec_dev *ec) { struct cros_ec_platform *ec_platform = dev_get_platdata(ec->dev); const char *name = ec_platform->ec_name; struct cros_ec_debugfs *debug_info; int ret; debug_info = devm_kzalloc(ec->dev, sizeof(*debug_info), GFP_KERNEL); if (!debug_info) return -ENOMEM; debug_info->ec = ec; debug_info->dir = debugfs_create_dir(name, NULL); if (!debug_info->dir) return -ENOMEM; ret = cros_ec_create_panicinfo(debug_info); if (ret) goto remove_debugfs; ret = cros_ec_create_console_log(debug_info); if (ret) goto remove_debugfs; ec->debug_info = debug_info; return 0; remove_debugfs: debugfs_remove_recursive(debug_info->dir); return ret; } void cros_ec_debugfs_remove(struct cros_ec_dev *ec) { if (!ec->debug_info) return; debugfs_remove_recursive(ec->debug_info->dir); cros_ec_cleanup_console_log(ec->debug_info); }