mirror of
https://git.proxmox.com/git/fwupd
synced 2025-05-02 00:37:35 +00:00
537 lines
12 KiB
C
537 lines
12 KiB
C
/*
|
|
* Copyright (C) 2017 Richard Hughes <richard@hughsie.com>
|
|
*
|
|
* SPDX-License-Identifier: LGPL-2.1+
|
|
*/
|
|
|
|
#define G_LOG_DOMAIN "FuIOChannel"
|
|
|
|
#include "config.h"
|
|
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <gio/gio.h>
|
|
#include <glib/gstdio.h>
|
|
#ifdef HAVE_POLL_H
|
|
#include <poll.h>
|
|
#endif
|
|
#include <string.h>
|
|
#include <sys/stat.h>
|
|
|
|
#include "fwupd-error.h"
|
|
|
|
#include "fu-common.h"
|
|
#include "fu-io-channel.h"
|
|
|
|
/**
|
|
* FuIOChannel:
|
|
*
|
|
* A bidirectional IO channel which can be read from and written to.
|
|
*/
|
|
|
|
struct _FuIOChannel {
|
|
GObject parent_instance;
|
|
gint fd;
|
|
};
|
|
|
|
G_DEFINE_TYPE(FuIOChannel, fu_io_channel, G_TYPE_OBJECT)
|
|
|
|
/**
|
|
* fu_io_channel_unix_get_fd:
|
|
* @self: a #FuIOChannel
|
|
*
|
|
* Gets the file descriptor for the device.
|
|
*
|
|
* Returns: fd, or -1 for not open.
|
|
*
|
|
* Since: 1.2.2
|
|
**/
|
|
gint
|
|
fu_io_channel_unix_get_fd(FuIOChannel *self)
|
|
{
|
|
g_return_val_if_fail(FU_IS_IO_CHANNEL(self), -1);
|
|
return self->fd;
|
|
}
|
|
|
|
/**
|
|
* fu_io_channel_shutdown:
|
|
* @self: a #FuIOChannel
|
|
* @error: (nullable): optional return location for an error
|
|
*
|
|
* Closes the file descriptor for the device.
|
|
*
|
|
* Returns: %TRUE if all the FD was closed.
|
|
*
|
|
* Since: 1.2.2
|
|
**/
|
|
gboolean
|
|
fu_io_channel_shutdown(FuIOChannel *self, GError **error)
|
|
{
|
|
g_return_val_if_fail(FU_IS_IO_CHANNEL(self), FALSE);
|
|
g_return_val_if_fail(error == NULL || *error == NULL, FALSE);
|
|
if (!g_close(self->fd, error))
|
|
return FALSE;
|
|
self->fd = -1;
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
fu_io_channel_flush_input(FuIOChannel *self, GError **error)
|
|
{
|
|
GPollFD poll = {
|
|
.fd = self->fd,
|
|
.events = G_IO_IN | G_IO_ERR,
|
|
};
|
|
while (g_poll(&poll, 1, 0) > 0) {
|
|
gchar c;
|
|
gint r = read(self->fd, &c, 1);
|
|
if (r < 0 && errno != EINTR)
|
|
break;
|
|
}
|
|
return TRUE;
|
|
}
|
|
|
|
/**
|
|
* fu_io_channel_write_bytes:
|
|
* @self: a #FuIOChannel
|
|
* @bytes: buffer to write
|
|
* @timeout_ms: timeout in ms
|
|
* @flags: channel flags, e.g. %FU_IO_CHANNEL_FLAG_SINGLE_SHOT
|
|
* @error: (nullable): optional return location for an error
|
|
*
|
|
* Writes bytes to the TTY, that will fail if exceeding @timeout_ms.
|
|
*
|
|
* Returns: %TRUE if all the bytes was written
|
|
*
|
|
* Since: 1.2.2
|
|
**/
|
|
gboolean
|
|
fu_io_channel_write_bytes(FuIOChannel *self,
|
|
GBytes *bytes,
|
|
guint timeout_ms,
|
|
FuIOChannelFlags flags,
|
|
GError **error)
|
|
{
|
|
gsize bufsz = 0;
|
|
const guint8 *buf = g_bytes_get_data(bytes, &bufsz);
|
|
return fu_io_channel_write_raw(self, buf, bufsz, timeout_ms, flags, error);
|
|
}
|
|
|
|
/**
|
|
* fu_io_channel_write_byte_array:
|
|
* @self: a #FuIOChannel
|
|
* @buf: buffer to write
|
|
* @timeout_ms: timeout in ms
|
|
* @flags: channel flags, e.g. %FU_IO_CHANNEL_FLAG_SINGLE_SHOT
|
|
* @error: (nullable): optional return location for an error
|
|
*
|
|
* Writes bytes to the TTY, that will fail if exceeding @timeout_ms.
|
|
*
|
|
* Returns: %TRUE if all the bytes was written
|
|
*
|
|
* Since: 1.3.2
|
|
**/
|
|
gboolean
|
|
fu_io_channel_write_byte_array(FuIOChannel *self,
|
|
GByteArray *buf,
|
|
guint timeout_ms,
|
|
FuIOChannelFlags flags,
|
|
GError **error)
|
|
{
|
|
return fu_io_channel_write_raw(self, buf->data, buf->len, timeout_ms, flags, error);
|
|
}
|
|
|
|
/**
|
|
* fu_io_channel_write_raw:
|
|
* @self: a #FuIOChannel
|
|
* @data: buffer to write
|
|
* @datasz: size of @data
|
|
* @timeout_ms: timeout in ms
|
|
* @flags: channel flags, e.g. %FU_IO_CHANNEL_FLAG_SINGLE_SHOT
|
|
* @error: (nullable): optional return location for an error
|
|
*
|
|
* Writes bytes to the TTY, that will fail if exceeding @timeout_ms.
|
|
*
|
|
* Returns: %TRUE if all the bytes was written
|
|
*
|
|
* Since: 1.2.2
|
|
**/
|
|
gboolean
|
|
fu_io_channel_write_raw(FuIOChannel *self,
|
|
const guint8 *data,
|
|
gsize datasz,
|
|
guint timeout_ms,
|
|
FuIOChannelFlags flags,
|
|
GError **error)
|
|
{
|
|
gsize idx = 0;
|
|
|
|
g_return_val_if_fail(FU_IS_IO_CHANNEL(self), FALSE);
|
|
g_return_val_if_fail(error == NULL || *error == NULL, FALSE);
|
|
|
|
/* flush pending reads */
|
|
if (flags & FU_IO_CHANNEL_FLAG_FLUSH_INPUT) {
|
|
if (!fu_io_channel_flush_input(self, error))
|
|
return FALSE;
|
|
}
|
|
|
|
/* blocking IO */
|
|
if (flags & FU_IO_CHANNEL_FLAG_USE_BLOCKING_IO) {
|
|
gssize wrote = write(self->fd, data, datasz);
|
|
if (wrote != (gssize)datasz) {
|
|
if (errno == EPROTO) {
|
|
g_set_error(error,
|
|
FWUPD_ERROR,
|
|
FWUPD_ERROR_NOT_FOUND,
|
|
"failed to write: %s",
|
|
strerror(errno));
|
|
return FALSE;
|
|
}
|
|
g_set_error(error,
|
|
G_IO_ERROR,
|
|
G_IO_ERROR_FAILED,
|
|
"failed to write: "
|
|
"wrote %" G_GSSIZE_FORMAT " of %" G_GSIZE_FORMAT,
|
|
wrote,
|
|
datasz);
|
|
return FALSE;
|
|
}
|
|
return TRUE;
|
|
}
|
|
|
|
/* nonblocking IO */
|
|
while (idx < datasz) {
|
|
gint rc;
|
|
GPollFD fds = {
|
|
.fd = self->fd,
|
|
.events = G_IO_OUT | G_IO_ERR,
|
|
};
|
|
|
|
/* wait for data to be allowed to write without blocking */
|
|
rc = g_poll(&fds, 1, (gint)timeout_ms);
|
|
if (rc == 0)
|
|
break;
|
|
if (rc < 0) {
|
|
g_set_error(error,
|
|
FWUPD_ERROR,
|
|
FWUPD_ERROR_READ,
|
|
"failed to poll %i",
|
|
self->fd);
|
|
return FALSE;
|
|
}
|
|
|
|
/* we can write data */
|
|
if (fds.revents & G_IO_OUT) {
|
|
gssize len = write(self->fd, data + idx, datasz - idx);
|
|
if (len < 0) {
|
|
if (errno == EAGAIN) {
|
|
g_debug("got EAGAIN, trying harder");
|
|
continue;
|
|
}
|
|
if (errno == EPROTO) {
|
|
g_set_error(error,
|
|
FWUPD_ERROR,
|
|
FWUPD_ERROR_NOT_FOUND,
|
|
"failed to write: %s",
|
|
strerror(errno));
|
|
return FALSE;
|
|
}
|
|
g_set_error(error,
|
|
FWUPD_ERROR,
|
|
FWUPD_ERROR_WRITE,
|
|
"failed to write %" G_GSIZE_FORMAT " bytes to %i: %s",
|
|
datasz,
|
|
self->fd,
|
|
strerror(errno));
|
|
return FALSE;
|
|
}
|
|
if (flags & FU_IO_CHANNEL_FLAG_SINGLE_SHOT)
|
|
break;
|
|
idx += len;
|
|
}
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
/**
|
|
* fu_io_channel_read_bytes:
|
|
* @self: a #FuIOChannel
|
|
* @max_size: maximum size of the returned blob, or -1 for no limit
|
|
* @timeout_ms: timeout in ms
|
|
* @flags: channel flags, e.g. %FU_IO_CHANNEL_FLAG_SINGLE_SHOT
|
|
* @error: (nullable): optional return location for an error
|
|
*
|
|
* Reads bytes from the TTY, that will fail if exceeding @timeout_ms.
|
|
*
|
|
* Returns: a #GBytes, or %NULL for error
|
|
*
|
|
* Since: 1.2.2
|
|
**/
|
|
GBytes *
|
|
fu_io_channel_read_bytes(FuIOChannel *self,
|
|
gssize max_size,
|
|
guint timeout_ms,
|
|
FuIOChannelFlags flags,
|
|
GError **error)
|
|
{
|
|
GByteArray *buf = fu_io_channel_read_byte_array(self, max_size, timeout_ms, flags, error);
|
|
if (buf == NULL)
|
|
return NULL;
|
|
return g_byte_array_free_to_bytes(buf);
|
|
}
|
|
|
|
/**
|
|
* fu_io_channel_read_byte_array:
|
|
* @self: a #FuIOChannel
|
|
* @max_size: maximum size of the returned blob, or -1 for no limit
|
|
* @timeout_ms: timeout in ms
|
|
* @flags: channel flags, e.g. %FU_IO_CHANNEL_FLAG_SINGLE_SHOT
|
|
* @error: (nullable): optional return location for an error
|
|
*
|
|
* Reads bytes from the TTY, that will fail if exceeding @timeout_ms.
|
|
*
|
|
* Returns: (transfer full): a #GByteArray, or %NULL for error
|
|
*
|
|
* Since: 1.3.2
|
|
**/
|
|
GByteArray *
|
|
fu_io_channel_read_byte_array(FuIOChannel *self,
|
|
gssize max_size,
|
|
guint timeout_ms,
|
|
FuIOChannelFlags flags,
|
|
GError **error)
|
|
{
|
|
GPollFD fds = {
|
|
.fd = self->fd,
|
|
.events = G_IO_IN | G_IO_PRI | G_IO_ERR,
|
|
};
|
|
g_autoptr(GByteArray) buf2 = g_byte_array_new();
|
|
|
|
g_return_val_if_fail(FU_IS_IO_CHANNEL(self), NULL);
|
|
|
|
/* blocking IO */
|
|
if (flags & FU_IO_CHANNEL_FLAG_USE_BLOCKING_IO) {
|
|
guint8 buf[1024];
|
|
gssize len = read(self->fd, buf, sizeof(buf));
|
|
if (len < 0) {
|
|
g_set_error(error,
|
|
FWUPD_ERROR,
|
|
FWUPD_ERROR_READ,
|
|
"failed to read %i: %s",
|
|
self->fd,
|
|
strerror(errno));
|
|
return NULL;
|
|
}
|
|
if (len > 0)
|
|
g_byte_array_append(buf2, buf, len);
|
|
return g_steal_pointer(&buf2);
|
|
}
|
|
|
|
/* nonblocking IO */
|
|
while (TRUE) {
|
|
/* wait for data to appear */
|
|
gint rc = g_poll(&fds, 1, (gint)timeout_ms);
|
|
if (rc == 0) {
|
|
g_set_error(error, G_IO_ERROR, G_IO_ERROR_TIMED_OUT, "timeout");
|
|
return NULL;
|
|
}
|
|
if (rc < 0) {
|
|
if (errno == EINTR)
|
|
continue;
|
|
g_set_error(error,
|
|
FWUPD_ERROR,
|
|
FWUPD_ERROR_READ,
|
|
"failed to poll %i",
|
|
self->fd);
|
|
return NULL;
|
|
}
|
|
|
|
/* we have data to read */
|
|
if (fds.revents & G_IO_IN) {
|
|
guint8 buf[1024];
|
|
gssize len = read(self->fd, buf, sizeof(buf));
|
|
if (len < 0) {
|
|
if (errno == EINTR)
|
|
continue;
|
|
if (errno == EAGAIN)
|
|
continue;
|
|
g_set_error(error,
|
|
FWUPD_ERROR,
|
|
FWUPD_ERROR_READ,
|
|
"failed to read %i: %s",
|
|
self->fd,
|
|
strerror(errno));
|
|
return NULL;
|
|
}
|
|
if (len > 0)
|
|
g_byte_array_append(buf2, buf, len);
|
|
|
|
/* check maximum size */
|
|
if (max_size > 0 && buf2->len >= (guint)max_size)
|
|
break;
|
|
if (flags & FU_IO_CHANNEL_FLAG_SINGLE_SHOT)
|
|
break;
|
|
continue;
|
|
}
|
|
if (fds.revents & G_IO_ERR) {
|
|
g_set_error_literal(error,
|
|
FWUPD_ERROR,
|
|
FWUPD_ERROR_READ,
|
|
"error condition");
|
|
return NULL;
|
|
}
|
|
if (fds.revents & G_IO_HUP) {
|
|
g_set_error_literal(error,
|
|
FWUPD_ERROR,
|
|
FWUPD_ERROR_READ,
|
|
"connection hung up");
|
|
return NULL;
|
|
}
|
|
if (fds.revents & G_IO_NVAL) {
|
|
g_set_error_literal(error,
|
|
FWUPD_ERROR,
|
|
FWUPD_ERROR_READ,
|
|
"invalid request");
|
|
return NULL;
|
|
}
|
|
}
|
|
|
|
/* no data */
|
|
if (buf2->len == 0) {
|
|
g_set_error(error,
|
|
FWUPD_ERROR,
|
|
FWUPD_ERROR_READ,
|
|
"no data received from device in %ums",
|
|
timeout_ms);
|
|
return NULL;
|
|
}
|
|
|
|
/* return blob */
|
|
return g_steal_pointer(&buf2);
|
|
}
|
|
|
|
/**
|
|
* fu_io_channel_read_raw:
|
|
* @self: a #FuIOChannel
|
|
* @buf: (nullable): optional buffer
|
|
* @bufsz: size of @buf
|
|
* @bytes_read: (out) (nullable): data written to @buf
|
|
* @timeout_ms: timeout in ms
|
|
* @flags: channel flags, e.g. %FU_IO_CHANNEL_FLAG_SINGLE_SHOT
|
|
* @error: (nullable): optional return location for an error
|
|
*
|
|
* Reads bytes from the TTY, that will fail if exceeding @timeout_ms.
|
|
*
|
|
* Returns: a #GBytes, or %NULL for error
|
|
*
|
|
* Since: 1.2.2
|
|
**/
|
|
gboolean
|
|
fu_io_channel_read_raw(FuIOChannel *self,
|
|
guint8 *buf,
|
|
gsize bufsz,
|
|
gsize *bytes_read,
|
|
guint timeout_ms,
|
|
FuIOChannelFlags flags,
|
|
GError **error)
|
|
{
|
|
const guint8 *tmpbuf = NULL;
|
|
gsize bytes_read_tmp;
|
|
g_autoptr(GBytes) tmp = NULL;
|
|
|
|
g_return_val_if_fail(FU_IS_IO_CHANNEL(self), FALSE);
|
|
|
|
tmp = fu_io_channel_read_bytes(self, bufsz, timeout_ms, flags, error);
|
|
if (tmp == NULL)
|
|
return FALSE;
|
|
tmpbuf = g_bytes_get_data(tmp, &bytes_read_tmp);
|
|
if (tmpbuf != NULL)
|
|
memcpy(buf, tmpbuf, bytes_read_tmp);
|
|
if (bytes_read != NULL)
|
|
*bytes_read = bytes_read_tmp;
|
|
return TRUE;
|
|
}
|
|
|
|
static void
|
|
fu_io_channel_finalize(GObject *object)
|
|
{
|
|
FuIOChannel *self = FU_IO_CHANNEL(object);
|
|
if (self->fd != -1)
|
|
g_close(self->fd, NULL);
|
|
G_OBJECT_CLASS(fu_io_channel_parent_class)->finalize(object);
|
|
}
|
|
|
|
static void
|
|
fu_io_channel_class_init(FuIOChannelClass *klass)
|
|
{
|
|
GObjectClass *object_class = G_OBJECT_CLASS(klass);
|
|
object_class->finalize = fu_io_channel_finalize;
|
|
}
|
|
|
|
static void
|
|
fu_io_channel_init(FuIOChannel *self)
|
|
{
|
|
self->fd = -1;
|
|
}
|
|
|
|
/**
|
|
* fu_io_channel_unix_new:
|
|
* @fd: file descriptor
|
|
*
|
|
* Creates a new object to write and read from.
|
|
*
|
|
* Returns: a #FuIOChannel
|
|
*
|
|
* Since: 1.2.2
|
|
**/
|
|
FuIOChannel *
|
|
fu_io_channel_unix_new(gint fd)
|
|
{
|
|
FuIOChannel *self;
|
|
self = g_object_new(FU_TYPE_IO_CHANNEL, NULL);
|
|
self->fd = fd;
|
|
return FU_IO_CHANNEL(self);
|
|
}
|
|
|
|
/**
|
|
* fu_io_channel_new_file:
|
|
* @filename: device file
|
|
* @error: (nullable): optional return location for an error
|
|
*
|
|
* Creates a new object to write and read from.
|
|
*
|
|
* Returns: a #FuIOChannel
|
|
*
|
|
* Since: 1.2.2
|
|
**/
|
|
FuIOChannel *
|
|
fu_io_channel_new_file(const gchar *filename, GError **error)
|
|
{
|
|
#ifdef HAVE_POLL_H
|
|
gint fd;
|
|
|
|
g_return_val_if_fail(filename != NULL, NULL);
|
|
g_return_val_if_fail(error == NULL || *error == NULL, NULL);
|
|
|
|
fd = g_open(filename, O_RDWR | O_NONBLOCK, S_IRWXU);
|
|
if (fd < 0) {
|
|
g_set_error(error,
|
|
FWUPD_ERROR,
|
|
FWUPD_ERROR_INVALID_FILE,
|
|
"failed to open %s",
|
|
filename);
|
|
return NULL;
|
|
}
|
|
return fu_io_channel_unix_new(fd);
|
|
#else
|
|
g_return_val_if_fail(filename != NULL, NULL);
|
|
g_return_val_if_fail(error == NULL || *error == NULL, NULL);
|
|
g_set_error_literal(error,
|
|
FWUPD_ERROR,
|
|
FWUPD_ERROR_NOT_SUPPORTED,
|
|
"Not supported as <poll.h> is unavailable");
|
|
return NULL;
|
|
#endif
|
|
}
|