wClipboard/posix: implement file range retrieval

This is another bunch of callbacks which provide the file contents to
the clients. We jump through some extra hoops in order to have more
pleasant user experience.

Simple stuff goes first. The file offset (or position) is split into the
low and high parts because this is the format in which the clients
receive the request from the server. They can simply copy the values as
is into the struct without repackaging them (which we do instead in the
end to get a 64-bit off_t).

Another thing is that we try to minimize the number of lseek() calls and
to keep as few file descriptors open as possible. We cannot open all the
files at once as there could be thousands of them and we'll run out of
the allowed number of the fds. However, the server can (in theory)
request the file ranges randomly so we need to be prepared for that. One
way to do that would be to always open the file before reading and close
it immediately afterwards. A dead simple solution with an acceptable
performance, but... some file systems do not support seeking, such as
FTP directories mounted over FUSE. However, they handle sequential
reading just fine *and* the server requests the data sequentially most
of the time so we can exploit this.

Thus open the file only once, during the first range request and keep
it open until the server reads all the data. In order to know how much
data is left we keep an accurate account of all reads and maintain the
file offset ourselves. This also allows us to avoid calling lseek() when
the file offset will not be effectively changed. However, if the server
requests some weird offset then we have no choice and will attempt
seeking. Unfortunately, we cannot tell whether it is a genuine failure
or the file system just does not support seeking, so we do not handle
the error further. (One workaround would be to reopen the file and keep
reading it until we're at the correct offset.) In this way we can
support sequential-only file systems if the server requests the contents
sequentially (and it does).

Also note that we do an fstat() right after opening the file in order to
have an accurate value of file size, for this exact file descriptor we
will be using. We should have it filled it by now, but just in case...

There is one more thing to explain. The cbRequested field specifies the
maximum number of bytes the server can handle, not the required number.
Usually this is some power-of-two number like 64 KB, based on the size
of the files on the clipboard. This is why posix_file_read_perform()
does not attempt to fill the whole buffer by repeatedly calling read()
if it has read less data than requested. The server can handle underruns
just fine (and this spares us from special-casing the EOF condition).
This commit is contained in:
ilammy 2017-04-09 02:29:51 +03:00
parent 33719d24ce
commit 092e870d2a
2 changed files with 224 additions and 0 deletions

View File

@ -34,6 +34,16 @@ struct _wClipboardFileSizeRequest
};
typedef struct _wClipboardFileSizeRequest wClipboardFileSizeRequest;
struct _wClipboardFileRangeRequest
{
UINT32 streamId;
UINT32 listIndex;
UINT32 nPositionLow;
UINT32 nPositionHigh;
UINT32 cbRequested;
};
typedef struct _wClipboardFileRangeRequest wClipboardFileRangeRequest;
typedef struct _wClipboardDelegate wClipboardDelegate;
struct _wClipboardDelegate
@ -46,6 +56,12 @@ struct _wClipboardDelegate
UINT64 fileSize);
UINT (*ClipboardFileSizeFailure)(wClipboardDelegate*, const wClipboardFileSizeRequest*,
UINT errorCode);
UINT (*ClientRequestFileRange)(wClipboardDelegate*, const wClipboardFileRangeRequest*);
UINT (*ClipboardFileRangeSuccess)(wClipboardDelegate*, const wClipboardFileRangeRequest*,
const BYTE* data, UINT32 size);
UINT (*ClipboardFileRangeFailure)(wClipboardDelegate*, const wClipboardFileRangeRequest*,
UINT errorCode);
};
#ifdef __cplusplus

View File

@ -28,6 +28,7 @@
#include <errno.h>
#include <dirent.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
@ -49,6 +50,9 @@ struct posix_file
char* local_name;
WCHAR* remote_name;
BOOL is_directory;
int fd;
off_t offset;
off_t size;
};
@ -61,6 +65,9 @@ static struct posix_file* make_posix_file(const char* local_name, const WCHAR* r
if (!file)
return NULL;
file->fd = -1;
file->offset = 0;
file->local_name = _strdup(local_name);
file->remote_name = _wcsdup(remote_name);
@ -94,6 +101,15 @@ static void free_posix_file(void* the_file)
if (!file)
return;
if (file->fd >= 0)
{
if (close(file->fd) < 0)
{
int err = errno;
WLog_WARN(TAG, "failed to close fd %d: %s", file->fd, strerror(err));
}
}
free(file->local_name);
free(file->remote_name);
free(file);
@ -633,6 +649,184 @@ static UINT posix_file_request_size(wClipboardDelegate* delegate,
return NO_ERROR;
}
static UINT posix_file_read_open(struct posix_file* file)
{
struct stat statbuf;
if (file->fd >= 0)
return NO_ERROR;
file->fd = open(file->local_name, O_RDONLY);
if (file->fd < 0)
{
int err = errno;
WLog_ERR(TAG, "failed to open file %s: %s", file->local_name, strerror(err));
return ERROR_FILE_NOT_FOUND;
}
if (fstat(file->fd, &statbuf) < 0)
{
int err = errno;
WLog_ERR(TAG, "failed to stat file: %s", strerror(err));
return ERROR_FILE_INVALID;
}
file->offset = 0;
file->size = statbuf.st_size;
#ifdef WITH_DEBUG_WCLIPBOARD
WLog_DBG(TAG, "open file %d -> %s", file->fd, file->local_name);
WLog_DBG(TAG, "file %d size: %"PRIu64" bytes", file->fd, file->size);
#endif
return NO_ERROR;
}
static UINT posix_file_read_seek(struct posix_file* file, UINT64 offset)
{
/*
* We should avoid seeking when possible as some filesystems (e.g.,
* an FTP server mapped via FUSE) may not support seeking. We keep
* an accurate account of the current file offset and do not call
* lseek() if the client requests file content sequentially.
*/
if (file->offset == offset)
return NO_ERROR;
#ifdef WITH_DEBUG_WCLIPBOARD
WLog_DBG(TAG, "file %d force seeking to %"PRIu64", current %"PRIu64, file->fd,
offset, file->offset);
#endif
if (lseek(file->fd, offset, SEEK_SET) < 0)
{
int err = errno;
WLog_ERR(TAG, "failed to seek file: %s", strerror(err));
return ERROR_SEEK;
}
return NO_ERROR;
}
static UINT posix_file_read_perform(struct posix_file* file, UINT32 size,
BYTE** actual_data, UINT32* actual_size)
{
BYTE* buffer = NULL;
ssize_t amount = 0;
#ifdef WITH_DEBUG_WCLIPBOARD
WLog_DBG(TAG, "file %d request read %"PRIu32" bytes", file->fd, size);
#endif
buffer = malloc(size);
if (!buffer)
{
WLog_ERR(TAG, "failed to allocate %"PRIu32" buffer bytes", size);
return ERROR_NOT_ENOUGH_MEMORY;
}
amount = read(file->fd, buffer, size);
if (amount < 0)
{
int err = errno;
WLog_ERR(TAG, "failed to read file: %s", strerror(err));
goto error;
}
*actual_data = buffer;
*actual_size = amount;
file->offset += amount;
#ifdef WITH_DEBUG_WCLIPBOARD
WLog_DBG(TAG, "file %d actual read %"PRIu32" bytes (offset %"PRIu64")", file->fd,
amount, file->offset);
#endif
return NO_ERROR;
error:
free(buffer);
return ERROR_READ_FAULT;
}
static UINT posix_file_read_close(struct posix_file* file)
{
if (file->fd < 0)
return NO_ERROR;
if (file->offset == file->size)
{
#ifdef WITH_DEBUG_WCLIPBOARD
WLog_DBG(TAG, "close file %d", file->fd);
#endif
if (close(file->fd) < 0)
{
int err = errno;
WLog_WARN(TAG, "failed to close fd %d: %s", file->fd, strerror(err));
}
file->fd = -1;
}
return NO_ERROR;
}
static UINT posix_file_get_range(struct posix_file* file, UINT64 offset, UINT32 size,
BYTE** actual_data, UINT32* actual_size)
{
UINT error = NO_ERROR;
error = posix_file_read_open(file);
if (error)
goto out;
error = posix_file_read_seek(file, offset);
if (error)
goto out;
error = posix_file_read_perform(file, size, actual_data, actual_size);
if (error)
goto out;
error = posix_file_read_close(file);
if (error)
goto out;
out:
return error;
}
static UINT posix_file_request_range(wClipboardDelegate* delegate,
const wClipboardFileRangeRequest* request)
{
UINT error = 0;
BYTE* data = NULL;
UINT32 size = 0;
UINT64 offset = 0;
struct posix_file* file = NULL;
if (!delegate || !delegate->clipboard || !request)
return ERROR_BAD_ARGUMENTS;
file = ArrayList_GetItem(delegate->clipboard->localFiles, request->listIndex);
if (!file)
return ERROR_INDEX_ABSENT;
offset = (((UINT64) request->nPositionHigh) << 32) | ((UINT64) request->nPositionLow);
error = posix_file_get_range(file, offset, request->cbRequested, &data, &size);
if (error)
error = delegate->ClipboardFileRangeFailure(delegate, request, error);
else
error = delegate->ClipboardFileRangeSuccess(delegate, request, data, size);
if (error)
WLog_WARN(TAG, "failed to report file range result: 0x%08X", error);
free(data);
return NO_ERROR;
}
static UINT dummy_file_size_success(wClipboardDelegate* delegate, const wClipboardFileSizeRequest* request, UINT64 fileSize)
{
return ERROR_NOT_SUPPORTED;
@ -643,11 +837,25 @@ static UINT dummy_file_size_failure(wClipboardDelegate* delegate, const wClipboa
return ERROR_NOT_SUPPORTED;
}
static UINT dummy_file_range_success(wClipboardDelegate* delegate, const wClipboardFileRangeRequest* request, const BYTE* data, UINT32 size)
{
return ERROR_NOT_SUPPORTED;
}
static UINT dummy_file_range_failure(wClipboardDelegate* delegate, const wClipboardFileRangeRequest* request, UINT errorCode)
{
return ERROR_NOT_SUPPORTED;
}
static void setup_delegate(wClipboardDelegate* delegate)
{
delegate->ClientRequestFileSize = posix_file_request_size;
delegate->ClipboardFileSizeSuccess = dummy_file_size_success;
delegate->ClipboardFileSizeFailure = dummy_file_size_failure;
delegate->ClientRequestFileRange = posix_file_request_range;
delegate->ClipboardFileRangeSuccess = dummy_file_range_success;
delegate->ClipboardFileRangeFailure = dummy_file_range_failure;
}
BOOL ClipboardInitPosixFileSubsystem(wClipboard* clipboard)