mirror of
https://github.com/gnif/LookingGlass.git
synced 2026-08-22 07:01:30 +00:00
3740 lines
104 KiB
C
3740 lines
104 KiB
C
/**
|
|
* Looking Glass
|
|
* Copyright © 2017-2026 The Looking Glass Authors
|
|
* https://looking-glass.io
|
|
*
|
|
* 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, write to the Free Software Foundation, Inc., 59
|
|
* Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
|
*/
|
|
|
|
#define FUSE_USE_VERSION 310
|
|
|
|
#include "core/clipboard_files.h"
|
|
#include "core/clipboard.h"
|
|
|
|
#include "common/KVMFRClipboard.h"
|
|
#include "common/debug.h"
|
|
#include "common/event.h"
|
|
#include "common/locking.h"
|
|
#include "common/thread.h"
|
|
#include "common/time.h"
|
|
|
|
#include <fuse3/fuse_lowlevel.h>
|
|
|
|
#include <dirent.h>
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <inttypes.h>
|
|
#include <limits.h>
|
|
#include <poll.h>
|
|
#include <stdatomic.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <strings.h>
|
|
#include <sys/eventfd.h>
|
|
#include <sys/random.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/statvfs.h>
|
|
#include <sys/types.h>
|
|
#include <unistd.h>
|
|
|
|
#define FILE_WORKERS 4U
|
|
#define FILE_ATTR_TIMEOUT 1.0
|
|
#define FILE_ENTRY_TIMEOUT 1.0
|
|
#define FILE_REQUEST_MAX KVMFR_CLIPBOARD_FILE_READ_BYTES
|
|
#define FILE_STREAM_CHUNK KVMFR_CLIPBOARD_DATA_BYTES
|
|
#define FILE_DELIVERY_GRACE_US UINT64_C(30000000)
|
|
#define FILE_DELIVERY_GRACE_MAX 4U
|
|
#define FILE_URI_PREFIX "file://"
|
|
#define FILE_GNOME_MIME "x-special/gnome-copied-files"
|
|
#define FILE_MATE_MIME "x-special/mate-copied-files"
|
|
#define FILE_URI_MIME "text/uri-list"
|
|
#define FILE_KDE_CUT_MIME "application/x-kde-cutselection"
|
|
|
|
_Static_assert(FILE_REQUEST_MAX <= UINT32_MAX,
|
|
"clipboard FUSE read size exceeds the FUSE protocol field");
|
|
|
|
enum RemoteReply
|
|
{
|
|
REMOTE_REPLY_INITIAL,
|
|
REMOTE_REPLY_LOOKUP,
|
|
REMOTE_REPLY_READDIR,
|
|
REMOTE_REPLY_READ,
|
|
};
|
|
|
|
struct RemoteDataset;
|
|
|
|
struct RemoteNode
|
|
{
|
|
struct RemoteNode * next;
|
|
struct RemoteDataset * dataset;
|
|
fuse_ino_t ino;
|
|
fuse_ino_t parent;
|
|
uint64_t node;
|
|
uint64_t size;
|
|
uint64_t createdNs;
|
|
uint64_t modifiedNs;
|
|
KVMFRClipboardFileType type;
|
|
uint64_t lookups;
|
|
bool listed;
|
|
char * name;
|
|
};
|
|
|
|
struct RemoteDataset
|
|
{
|
|
struct RemoteDataset * next;
|
|
uint64_t dataset;
|
|
uint64_t acquisition;
|
|
fuse_ino_t ino;
|
|
uint64_t lookupPins;
|
|
uint64_t openPins;
|
|
uint64_t requestPins;
|
|
uint64_t operationPins;
|
|
uint64_t deliveryDeadline;
|
|
unsigned presentationPins;
|
|
bool acquiring;
|
|
bool acquired;
|
|
bool ready;
|
|
bool current;
|
|
bool stale;
|
|
bool deliveryPending;
|
|
bool pruneQueued;
|
|
char name[40];
|
|
};
|
|
|
|
struct RemoteRequest
|
|
{
|
|
struct RemoteRequest * next;
|
|
LG_ClipboardFileRequest request;
|
|
struct RemoteDataset * dataset;
|
|
struct RemoteNode * directory;
|
|
enum RemoteReply reply;
|
|
fuse_req_t fuseRequest;
|
|
size_t fuseSize;
|
|
off_t fuseOffset;
|
|
char * lookupName;
|
|
uint8_t * data;
|
|
size_t dataSize;
|
|
size_t dataCapacity;
|
|
uint64_t sizeHint;
|
|
bool began;
|
|
bool transportStarted;
|
|
};
|
|
|
|
enum RemoteActionType
|
|
{
|
|
REMOTE_ACTION_NONE,
|
|
REMOTE_ACTION_CANCEL_ACQUISITION,
|
|
REMOTE_ACTION_RELEASE_ACQUISITION,
|
|
};
|
|
|
|
struct RemoteAction
|
|
{
|
|
enum RemoteActionType type;
|
|
struct RemoteDataset * object;
|
|
uint64_t dataset;
|
|
uint64_t acquisition;
|
|
};
|
|
|
|
struct LocalDataset;
|
|
|
|
struct LocalNode
|
|
{
|
|
struct LocalNode * next;
|
|
struct LocalDataset * dataset;
|
|
struct LocalNode * root;
|
|
uint64_t node;
|
|
uint64_t parent;
|
|
uint64_t size;
|
|
uint64_t createdNs;
|
|
uint64_t modifiedNs;
|
|
KVMFRClipboardFileType type;
|
|
dev_t device;
|
|
ino_t inode;
|
|
int fd;
|
|
char * path;
|
|
char * name;
|
|
};
|
|
|
|
struct LocalDataset
|
|
{
|
|
struct LocalDataset * next;
|
|
struct LocalNode * nodes;
|
|
uint64_t nodeSerial;
|
|
uint64_t dataset;
|
|
unsigned references;
|
|
bool current;
|
|
};
|
|
|
|
struct LocalAcquisition
|
|
{
|
|
struct LocalAcquisition * next;
|
|
struct LocalDataset * dataset;
|
|
uint64_t wireDataset;
|
|
uint64_t acquisition;
|
|
};
|
|
|
|
struct LocalJob
|
|
{
|
|
struct LocalJob * next;
|
|
struct LocalDataset * dataset;
|
|
LG_ClipboardFileRequest request;
|
|
LGEvent * ready;
|
|
atomic_bool cancelled;
|
|
};
|
|
|
|
static struct
|
|
{
|
|
LG_Lock lock;
|
|
struct fuse_session * session;
|
|
LGThread * fuseThread;
|
|
int fuseStopFd;
|
|
char mountpoint[PATH_MAX];
|
|
bool initialized;
|
|
bool stopping;
|
|
fuse_ino_t inodeSerial;
|
|
uint64_t requestSerial;
|
|
uint64_t datasetSerial;
|
|
struct RemoteDataset * remoteDatasets;
|
|
struct RemoteDataset * remoteCurrent;
|
|
struct RemoteNode * remoteNodes;
|
|
struct RemoteRequest * remoteRequests;
|
|
struct LocalDataset * localDatasets;
|
|
struct LocalDataset * localCurrent;
|
|
struct LocalAcquisition * localAcquisitions;
|
|
struct LocalJob * jobsHead;
|
|
struct LocalJob ** jobsTail;
|
|
struct LocalJob * activeJobs;
|
|
LGEvent * jobEvent;
|
|
LGThread * workers[FILE_WORKERS];
|
|
}
|
|
files;
|
|
|
|
static bool filesLockInitialized;
|
|
|
|
#ifdef ENABLE_TESTS
|
|
static atomic_bool forceLocalEof;
|
|
#endif
|
|
|
|
static bool randomBytes(void * buffer, size_t size)
|
|
{
|
|
uint8_t * bytes = buffer;
|
|
size_t offset = 0;
|
|
while (offset < size)
|
|
{
|
|
const ssize_t result = getrandom(bytes + offset, size - offset, 0);
|
|
if (result < 0 && errno == EINTR)
|
|
continue;
|
|
if (result <= 0)
|
|
{
|
|
if (!result)
|
|
errno = EIO;
|
|
return false;
|
|
}
|
|
offset += (size_t)result;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static uint64_t nsFromTimespec(struct timespec value)
|
|
{
|
|
if (value.tv_sec < 0 || value.tv_nsec < 0)
|
|
return 0;
|
|
if ((uint64_t)value.tv_sec > UINT64_MAX / UINT64_C(1000000000))
|
|
return UINT64_MAX;
|
|
return (uint64_t)value.tv_sec * UINT64_C(1000000000) +
|
|
(uint64_t)value.tv_nsec;
|
|
}
|
|
|
|
static struct timespec timespecFromNs(uint64_t value)
|
|
{
|
|
return (struct timespec)
|
|
{
|
|
.tv_sec = (time_t)(value / UINT64_C(1000000000)),
|
|
.tv_nsec = (long)(value % UINT64_C(1000000000)),
|
|
};
|
|
}
|
|
|
|
static uint64_t nextRequestNL(void)
|
|
{
|
|
do
|
|
++files.requestSerial;
|
|
while (!files.requestSerial ||
|
|
kvmfrClipboardTransferFromHelper(files.requestSerial));
|
|
return files.requestSerial;
|
|
}
|
|
|
|
static uint64_t nextAcquisitionNL(void)
|
|
{
|
|
return nextRequestNL();
|
|
}
|
|
|
|
static uint64_t nextDatasetNL(void)
|
|
{
|
|
do
|
|
++files.datasetSerial;
|
|
while (!files.datasetSerial ||
|
|
kvmfrClipboardTransferFromHelper(files.datasetSerial));
|
|
return files.datasetSerial;
|
|
}
|
|
|
|
static fuse_ino_t nextInodeNL(void)
|
|
{
|
|
if (++files.inodeSerial <= FUSE_ROOT_ID)
|
|
files.inodeSerial = FUSE_ROOT_ID + 1;
|
|
return files.inodeSerial;
|
|
}
|
|
|
|
static int errnoFromFileError(LG_ClipboardFileError error)
|
|
{
|
|
switch (error)
|
|
{
|
|
case LG_CLIPBOARD_FILE_ERROR_NONE: return 0;
|
|
case LG_CLIPBOARD_FILE_ERROR_NOT_FOUND: return ENOENT;
|
|
case LG_CLIPBOARD_FILE_ERROR_ACCESS: return EACCES;
|
|
case LG_CLIPBOARD_FILE_ERROR_NOT_DIRECTORY: return ENOTDIR;
|
|
case LG_CLIPBOARD_FILE_ERROR_IS_DIRECTORY: return EISDIR;
|
|
case LG_CLIPBOARD_FILE_ERROR_NO_MEMORY: return ENOMEM;
|
|
case LG_CLIPBOARD_FILE_ERROR_NO_SPACE: return ENOSPC;
|
|
case LG_CLIPBOARD_FILE_ERROR_NOT_SUPPORTED: return ENOTSUP;
|
|
case LG_CLIPBOARD_FILE_ERROR_CANCELLED: return EINTR;
|
|
case LG_CLIPBOARD_FILE_ERROR_STALE: return ESTALE;
|
|
default: return EIO;
|
|
}
|
|
}
|
|
|
|
static const char * fileErrorName(LG_ClipboardFileError error)
|
|
{
|
|
switch (error)
|
|
{
|
|
case LG_CLIPBOARD_FILE_ERROR_NONE: return "none";
|
|
case LG_CLIPBOARD_FILE_ERROR_NOT_FOUND: return "not found";
|
|
case LG_CLIPBOARD_FILE_ERROR_ACCESS: return "access denied";
|
|
case LG_CLIPBOARD_FILE_ERROR_NOT_DIRECTORY: return "not a directory";
|
|
case LG_CLIPBOARD_FILE_ERROR_IS_DIRECTORY: return "is a directory";
|
|
case LG_CLIPBOARD_FILE_ERROR_IO: return "I/O error";
|
|
case LG_CLIPBOARD_FILE_ERROR_INVALID: return "invalid";
|
|
case LG_CLIPBOARD_FILE_ERROR_NO_MEMORY: return "out of memory";
|
|
case LG_CLIPBOARD_FILE_ERROR_NO_SPACE: return "no space";
|
|
case LG_CLIPBOARD_FILE_ERROR_DISCONNECTED: return "disconnected";
|
|
case LG_CLIPBOARD_FILE_ERROR_CANCELLED: return "cancelled";
|
|
case LG_CLIPBOARD_FILE_ERROR_NOT_SUPPORTED: return "not supported";
|
|
case LG_CLIPBOARD_FILE_ERROR_STALE: return "stale";
|
|
}
|
|
return "unknown";
|
|
}
|
|
|
|
static LG_ClipboardFileError fileErrorFromErrno(int error)
|
|
{
|
|
switch (error)
|
|
{
|
|
case ENOENT: return LG_CLIPBOARD_FILE_ERROR_NOT_FOUND;
|
|
case EACCES:
|
|
case EPERM: return LG_CLIPBOARD_FILE_ERROR_ACCESS;
|
|
case ENOTDIR: return LG_CLIPBOARD_FILE_ERROR_NOT_DIRECTORY;
|
|
case EISDIR: return LG_CLIPBOARD_FILE_ERROR_IS_DIRECTORY;
|
|
case ENOMEM:
|
|
case EMFILE:
|
|
case ENFILE: return LG_CLIPBOARD_FILE_ERROR_NO_MEMORY;
|
|
case ENOSPC: return LG_CLIPBOARD_FILE_ERROR_NO_SPACE;
|
|
case ELOOP:
|
|
case ENOTSUP: return LG_CLIPBOARD_FILE_ERROR_NOT_SUPPORTED;
|
|
case ESTALE: return LG_CLIPBOARD_FILE_ERROR_STALE;
|
|
case ECANCELED: return LG_CLIPBOARD_FILE_ERROR_CANCELLED;
|
|
default: return LG_CLIPBOARD_FILE_ERROR_IO;
|
|
}
|
|
}
|
|
|
|
static bool grow(void ** buffer, size_t * capacity, size_t wanted)
|
|
{
|
|
if (wanted <= *capacity)
|
|
return !wanted || *buffer;
|
|
size_t next = *capacity ? *capacity : 4096U;
|
|
while (next < wanted)
|
|
{
|
|
if (next > SIZE_MAX / 2U)
|
|
{
|
|
next = wanted;
|
|
break;
|
|
}
|
|
next *= 2U;
|
|
}
|
|
void * resized = realloc(*buffer, next);
|
|
if (!resized)
|
|
return false;
|
|
*buffer = resized;
|
|
*capacity = next;
|
|
return true;
|
|
}
|
|
|
|
static struct RemoteDataset * remoteDatasetByIdNL(uint64_t dataset)
|
|
{
|
|
for (struct RemoteDataset * item = files.remoteDatasets;
|
|
item; item = item->next)
|
|
if (item->dataset == dataset)
|
|
return item;
|
|
return NULL;
|
|
}
|
|
|
|
static struct RemoteDataset * remoteDatasetByAcquisitionNL(
|
|
uint64_t acquisition)
|
|
{
|
|
for (struct RemoteDataset * item = files.remoteDatasets;
|
|
item; item = item->next)
|
|
if (item->acquisition == acquisition)
|
|
return item;
|
|
return NULL;
|
|
}
|
|
|
|
static struct RemoteAction retireRemoteDatasetNL(
|
|
struct RemoteDataset * dataset)
|
|
{
|
|
struct RemoteAction action = { 0 };
|
|
if (!dataset || dataset->current || dataset->presentationPins ||
|
|
dataset->lookupPins || dataset->openPins || dataset->requestPins ||
|
|
dataset->operationPins || dataset->deliveryPending)
|
|
return action;
|
|
|
|
if (!dataset->stale)
|
|
{
|
|
dataset->ready = false;
|
|
dataset->stale = true;
|
|
action.dataset = dataset->dataset;
|
|
action.acquisition = dataset->acquisition;
|
|
if (dataset->acquired)
|
|
action.type = REMOTE_ACTION_RELEASE_ACQUISITION;
|
|
else if (dataset->acquiring)
|
|
action.type = REMOTE_ACTION_CANCEL_ACQUISITION;
|
|
dataset->acquiring = false;
|
|
dataset->acquired = false;
|
|
}
|
|
if (dataset->pruneQueued)
|
|
return action;
|
|
dataset->pruneQueued = true;
|
|
action.object = dataset;
|
|
return action;
|
|
}
|
|
|
|
/* Retire a failed initial acquisition/list only while it is still the
|
|
* dataset backing the current remote file offer. The caller must hold
|
|
* files.lock and subsequently call retireRemoteDatasetNL so an acquired
|
|
* dataset is released before it is pruned. */
|
|
static uint64_t failCurrentRemoteDatasetNL(
|
|
struct RemoteDataset * dataset)
|
|
{
|
|
if (!dataset || !dataset->current)
|
|
return 0;
|
|
|
|
dataset->current = false;
|
|
dataset->ready = false;
|
|
dataset->deliveryPending = false;
|
|
dataset->deliveryDeadline = 0;
|
|
if (files.remoteCurrent == dataset)
|
|
files.remoteCurrent = NULL;
|
|
return dataset->dataset;
|
|
}
|
|
|
|
static void pruneRemoteDataset(struct RemoteDataset * dataset)
|
|
{
|
|
if (!dataset)
|
|
return;
|
|
LG_LOCK(files.lock);
|
|
if (!dataset->pruneQueued || !dataset->stale || dataset->current ||
|
|
dataset->presentationPins || dataset->lookupPins ||
|
|
dataset->openPins || dataset->requestPins ||
|
|
dataset->operationPins || dataset->deliveryPending)
|
|
{
|
|
dataset->pruneQueued = false;
|
|
LG_UNLOCK(files.lock);
|
|
return;
|
|
}
|
|
|
|
struct RemoteDataset ** datasetLink = &files.remoteDatasets;
|
|
while (*datasetLink && *datasetLink != dataset)
|
|
datasetLink = &(*datasetLink)->next;
|
|
if (!*datasetLink)
|
|
{
|
|
LG_UNLOCK(files.lock);
|
|
return;
|
|
}
|
|
*datasetLink = dataset->next;
|
|
if (files.remoteCurrent == dataset)
|
|
files.remoteCurrent = NULL;
|
|
|
|
struct RemoteNode * retired = NULL;
|
|
struct RemoteNode ** nodeLink = &files.remoteNodes;
|
|
while (*nodeLink)
|
|
{
|
|
struct RemoteNode * node = *nodeLink;
|
|
if (node->dataset != dataset)
|
|
{
|
|
nodeLink = &node->next;
|
|
continue;
|
|
}
|
|
*nodeLink = node->next;
|
|
node->next = retired;
|
|
retired = node;
|
|
}
|
|
LG_UNLOCK(files.lock);
|
|
|
|
while (retired)
|
|
{
|
|
struct RemoteNode * next = retired->next;
|
|
free(retired->name);
|
|
free(retired);
|
|
retired = next;
|
|
}
|
|
free(dataset);
|
|
}
|
|
|
|
static void performRemoteAction(struct RemoteAction action)
|
|
{
|
|
switch (action.type)
|
|
{
|
|
case REMOTE_ACTION_CANCEL_ACQUISITION:
|
|
if (!clipboard_fileCancel(action.dataset, action.acquisition,
|
|
LG_CLIPBOARD_FILE_ERROR_CANCELLED))
|
|
DEBUG_WARN("Failed to cancel retired clipboard file acquisition");
|
|
break;
|
|
|
|
case REMOTE_ACTION_RELEASE_ACQUISITION:
|
|
if (!clipboard_fileRelease(action.dataset, action.acquisition))
|
|
DEBUG_WARN("Failed to release retired clipboard file acquisition");
|
|
break;
|
|
|
|
case REMOTE_ACTION_NONE:
|
|
break;
|
|
}
|
|
pruneRemoteDataset(action.object);
|
|
}
|
|
|
|
static bool pinRemoteOperationNL(
|
|
struct RemoteDataset * dataset, bool consumeDelivery)
|
|
{
|
|
if (!dataset || dataset->stale)
|
|
return false;
|
|
++dataset->operationPins;
|
|
if (consumeDelivery)
|
|
{
|
|
dataset->deliveryPending = false;
|
|
dataset->deliveryDeadline = 0;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static void releaseRemoteOperation(struct RemoteDataset * dataset)
|
|
{
|
|
if (!dataset)
|
|
return;
|
|
LG_LOCK(files.lock);
|
|
if (dataset->operationPins)
|
|
--dataset->operationPins;
|
|
const struct RemoteAction action = retireRemoteDatasetNL(dataset);
|
|
LG_UNLOCK(files.lock);
|
|
performRemoteAction(action);
|
|
}
|
|
|
|
static void reapRemoteDeliveries(bool pressure)
|
|
{
|
|
for (;;)
|
|
{
|
|
const uint64_t now = microtime();
|
|
LG_LOCK(files.lock);
|
|
struct RemoteDataset * candidate = NULL;
|
|
struct RemoteDataset * oldest = NULL;
|
|
unsigned pending = 0;
|
|
for (struct RemoteDataset * dataset = files.remoteDatasets;
|
|
dataset; dataset = dataset->next)
|
|
{
|
|
if (!dataset->deliveryPending)
|
|
continue;
|
|
if (dataset->deliveryDeadline <= now)
|
|
{
|
|
candidate = dataset;
|
|
break;
|
|
}
|
|
if (dataset->current)
|
|
continue;
|
|
++pending;
|
|
if (!oldest || dataset->deliveryDeadline < oldest->deliveryDeadline)
|
|
oldest = dataset;
|
|
}
|
|
if (!candidate && pressure && pending > FILE_DELIVERY_GRACE_MAX)
|
|
candidate = oldest;
|
|
if (!candidate)
|
|
{
|
|
LG_UNLOCK(files.lock);
|
|
return;
|
|
}
|
|
candidate->deliveryPending = false;
|
|
candidate->deliveryDeadline = 0;
|
|
const struct RemoteAction action = retireRemoteDatasetNL(candidate);
|
|
LG_UNLOCK(files.lock);
|
|
performRemoteAction(action);
|
|
}
|
|
}
|
|
|
|
static int nextRemoteDeliveryTimeout(void)
|
|
{
|
|
const uint64_t now = microtime();
|
|
uint64_t deadline = UINT64_MAX;
|
|
LG_LOCK(files.lock);
|
|
for (struct RemoteDataset * dataset = files.remoteDatasets;
|
|
dataset; dataset = dataset->next)
|
|
if (dataset->deliveryPending &&
|
|
dataset->deliveryDeadline < deadline)
|
|
deadline = dataset->deliveryDeadline;
|
|
LG_UNLOCK(files.lock);
|
|
if (deadline == UINT64_MAX)
|
|
return -1;
|
|
if (deadline <= now)
|
|
return 0;
|
|
const uint64_t delta = deadline - now;
|
|
const uint64_t milliseconds = delta / UINT64_C(1000) +
|
|
(delta % UINT64_C(1000) != 0);
|
|
return milliseconds > INT_MAX ? INT_MAX : (int)milliseconds;
|
|
}
|
|
|
|
static void pruneUnusedRemoteDatasets(void)
|
|
{
|
|
for (;;)
|
|
{
|
|
LG_LOCK(files.lock);
|
|
struct RemoteDataset * dataset = files.remoteDatasets;
|
|
while (dataset && (dataset->current || dataset->presentationPins ||
|
|
dataset->lookupPins || dataset->openPins || dataset->requestPins ||
|
|
dataset->operationPins || dataset->deliveryPending ||
|
|
dataset->pruneQueued))
|
|
dataset = dataset->next;
|
|
const struct RemoteAction action = retireRemoteDatasetNL(dataset);
|
|
LG_UNLOCK(files.lock);
|
|
if (!action.object)
|
|
return;
|
|
performRemoteAction(action);
|
|
}
|
|
}
|
|
|
|
static struct RemoteNode * remoteNodeByInodeNL(fuse_ino_t ino)
|
|
{
|
|
for (struct RemoteNode * node = files.remoteNodes; node; node = node->next)
|
|
if (node->ino == ino)
|
|
return node;
|
|
return NULL;
|
|
}
|
|
|
|
static struct RemoteNode * remoteChildNL(struct RemoteDataset * dataset,
|
|
fuse_ino_t parent, const char * name)
|
|
{
|
|
for (struct RemoteNode * node = files.remoteNodes; node; node = node->next)
|
|
if (node->dataset == dataset && node->parent == parent &&
|
|
!strcmp(node->name, name))
|
|
return node;
|
|
return NULL;
|
|
}
|
|
|
|
static struct RemoteNode * remoteNodeByWireNL(
|
|
struct RemoteDataset * dataset, uint64_t wireNode)
|
|
{
|
|
for (struct RemoteNode * node = files.remoteNodes; node; node = node->next)
|
|
if (node->dataset == dataset && node->node == wireNode)
|
|
return node;
|
|
return NULL;
|
|
}
|
|
|
|
static bool validUtf8(const uint8_t * value, size_t length)
|
|
{
|
|
for (size_t i = 0; i < length;)
|
|
{
|
|
const uint8_t first = value[i++];
|
|
if (first < 0x80)
|
|
continue;
|
|
unsigned continuation;
|
|
uint32_t codepoint;
|
|
uint32_t minimum;
|
|
if ((first & 0xe0U) == 0xc0U)
|
|
{
|
|
continuation = 1;
|
|
codepoint = first & 0x1fU;
|
|
minimum = 0x80;
|
|
}
|
|
else if ((first & 0xf0U) == 0xe0U)
|
|
{
|
|
continuation = 2;
|
|
codepoint = first & 0x0fU;
|
|
minimum = 0x800;
|
|
}
|
|
else if ((first & 0xf8U) == 0xf0U)
|
|
{
|
|
continuation = 3;
|
|
codepoint = first & 0x07U;
|
|
minimum = 0x10000;
|
|
}
|
|
else
|
|
return false;
|
|
if (continuation > length - i)
|
|
return false;
|
|
while (continuation--)
|
|
{
|
|
const uint8_t next = value[i++];
|
|
if ((next & 0xc0U) != 0x80U)
|
|
return false;
|
|
codepoint = (codepoint << 6) | (next & 0x3fU);
|
|
}
|
|
if (codepoint < minimum || codepoint > 0x10ffffU ||
|
|
(codepoint >= 0xd800U && codepoint <= 0xdfffU))
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static struct RemoteRequest * remoteRequestTakeNL(uint64_t request)
|
|
{
|
|
struct RemoteRequest ** link = &files.remoteRequests;
|
|
while (*link && (*link)->request.request != request)
|
|
link = &(*link)->next;
|
|
if (!*link)
|
|
return NULL;
|
|
struct RemoteRequest * result = *link;
|
|
*link = result->next;
|
|
result->next = NULL;
|
|
return result;
|
|
}
|
|
|
|
static struct RemoteRequest * remoteRequestTakePointerNL(
|
|
struct RemoteRequest * request)
|
|
{
|
|
struct RemoteRequest ** link = &files.remoteRequests;
|
|
while (*link && *link != request)
|
|
link = &(*link)->next;
|
|
if (!*link)
|
|
return NULL;
|
|
*link = request->next;
|
|
request->next = NULL;
|
|
return request;
|
|
}
|
|
|
|
static struct RemoteAction completeRemoteRequestNL(
|
|
struct RemoteRequest * request)
|
|
{
|
|
if (request && request->dataset->requestPins)
|
|
--request->dataset->requestPins;
|
|
return request ? retireRemoteDatasetNL(request->dataset) :
|
|
(struct RemoteAction) { 0 };
|
|
}
|
|
|
|
static void freeRemoteRequest(struct RemoteRequest * request)
|
|
{
|
|
if (!request)
|
|
return;
|
|
free(request->lookupName);
|
|
free(request->data);
|
|
free(request);
|
|
}
|
|
|
|
static void unregisterRemoteInterrupt(struct RemoteRequest * request)
|
|
{
|
|
if (request && request->fuseRequest)
|
|
fuse_req_interrupt_func(request->fuseRequest, NULL, NULL);
|
|
}
|
|
|
|
static void failRemoteRequest(struct RemoteRequest * request, int error)
|
|
{
|
|
if (!request)
|
|
return;
|
|
if (request->reply == REMOTE_REPLY_INITIAL)
|
|
DEBUG_WARN("Remote clipboard file root list failed: "
|
|
"dataset=%lu, request=%lu, error=%d (%s)",
|
|
request->request.dataset, request->request.request,
|
|
error, strerror(error));
|
|
unregisterRemoteInterrupt(request);
|
|
if (request->fuseRequest)
|
|
fuse_reply_err(request->fuseRequest, error);
|
|
LG_LOCK(files.lock);
|
|
const uint64_t failedDataset = request->reply == REMOTE_REPLY_INITIAL ?
|
|
failCurrentRemoteDatasetNL(request->dataset) : 0;
|
|
const struct RemoteAction action = completeRemoteRequestNL(request);
|
|
LG_UNLOCK(files.lock);
|
|
freeRemoteRequest(request);
|
|
performRemoteAction(action);
|
|
if (failedDataset)
|
|
clipboard_fileRemoteFailed(failedDataset);
|
|
}
|
|
|
|
static void completeRemoteRequest(struct RemoteRequest * request)
|
|
{
|
|
LG_LOCK(files.lock);
|
|
const struct RemoteAction action = completeRemoteRequestNL(request);
|
|
LG_UNLOCK(files.lock);
|
|
freeRemoteRequest(request);
|
|
performRemoteAction(action);
|
|
}
|
|
|
|
static void fillStat(const struct RemoteNode * node, struct stat * value)
|
|
{
|
|
memset(value, 0, sizeof(*value));
|
|
value->st_ino = node->ino;
|
|
value->st_mode = node->type == KVMFR_CLIPBOARD_FILE_TYPE_DIRECTORY ?
|
|
S_IFDIR | 0500 : S_IFREG | 0400;
|
|
value->st_nlink = node->type == KVMFR_CLIPBOARD_FILE_TYPE_DIRECTORY ? 2 : 1;
|
|
value->st_uid = getuid();
|
|
value->st_gid = getgid();
|
|
value->st_size = node->size > INT64_MAX ? INT64_MAX : (off_t)node->size;
|
|
value->st_mtim = timespecFromNs(node->modifiedNs);
|
|
value->st_ctim = value->st_mtim;
|
|
value->st_atim = value->st_mtim;
|
|
}
|
|
|
|
static void replyEntry(fuse_req_t request, struct RemoteNode * node)
|
|
{
|
|
struct fuse_entry_param entry =
|
|
{
|
|
.ino = node->ino,
|
|
.generation = 1,
|
|
.attr_timeout = FILE_ATTR_TIMEOUT,
|
|
.entry_timeout = FILE_ENTRY_TIMEOUT,
|
|
};
|
|
fillStat(node, &entry.attr);
|
|
LG_LOCK(files.lock);
|
|
const bool available = !node->dataset->stale;
|
|
if (available)
|
|
{
|
|
++node->lookups;
|
|
++node->dataset->lookupPins;
|
|
}
|
|
LG_UNLOCK(files.lock);
|
|
if (!available)
|
|
{
|
|
fuse_reply_err(request, ESTALE);
|
|
return;
|
|
}
|
|
if (fuse_reply_entry(request, &entry) == 0)
|
|
return;
|
|
|
|
LG_LOCK(files.lock);
|
|
if (node->lookups)
|
|
--node->lookups;
|
|
if (node->dataset->lookupPins)
|
|
--node->dataset->lookupPins;
|
|
const struct RemoteAction action = retireRemoteDatasetNL(node->dataset);
|
|
LG_UNLOCK(files.lock);
|
|
performRemoteAction(action);
|
|
}
|
|
|
|
static bool installRemoteListNL(struct RemoteRequest * request)
|
|
{
|
|
struct RemoteNode * additions = NULL;
|
|
size_t offset = 0;
|
|
while (offset < request->dataSize)
|
|
{
|
|
if (request->dataSize - offset < sizeof(KVMFRClipboardFileEntry))
|
|
goto invalid;
|
|
KVMFRClipboardFileEntry entry;
|
|
memcpy(&entry, request->data + offset, sizeof(entry));
|
|
if (!entry.node || !entry.nameLength || entry.nameLength > NAME_MAX ||
|
|
(entry.type != KVMFR_CLIPBOARD_FILE_TYPE_REGULAR &&
|
|
entry.type != KVMFR_CLIPBOARD_FILE_TYPE_DIRECTORY))
|
|
goto invalid;
|
|
const uint64_t bytes64 = KVMFR_CLIPBOARD_FILE_ENTRY_BYTES(
|
|
entry.nameLength);
|
|
if (bytes64 > SIZE_MAX || bytes64 > request->dataSize - offset)
|
|
goto invalid;
|
|
const uint8_t * encodedName = request->data + offset +
|
|
sizeof(entry);
|
|
if (memchr(encodedName, 0, entry.nameLength) ||
|
|
!validUtf8(encodedName, entry.nameLength) ||
|
|
(entry.nameLength == 1 && encodedName[0] == '.') ||
|
|
(entry.nameLength == 2 && encodedName[0] == '.' &&
|
|
encodedName[1] == '.') ||
|
|
memchr(encodedName, '/', entry.nameLength))
|
|
goto invalid;
|
|
for (size_t i = sizeof(entry) + entry.nameLength;
|
|
i < (size_t)bytes64; ++i)
|
|
if (request->data[offset + i])
|
|
goto invalid;
|
|
char * name = malloc((size_t)entry.nameLength + 1U);
|
|
if (!name)
|
|
goto invalid;
|
|
memcpy(name, encodedName, entry.nameLength);
|
|
name[entry.nameLength] = 0;
|
|
|
|
struct RemoteNode * existing = remoteChildNL(
|
|
request->dataset, request->directory->ino, name);
|
|
struct RemoteNode * wireNode = remoteNodeByWireNL(
|
|
request->dataset, entry.node);
|
|
for (struct RemoteNode * node = additions; node; node = node->next)
|
|
{
|
|
if (node->parent == request->directory->ino &&
|
|
!strcmp(node->name, name))
|
|
existing = node;
|
|
if (node->node == entry.node)
|
|
wireNode = node;
|
|
}
|
|
|
|
if (existing || wireNode)
|
|
{
|
|
const bool same = existing && existing == wireNode &&
|
|
existing->size == entry.size &&
|
|
existing->createdNs == entry.createdNs &&
|
|
existing->modifiedNs == entry.modifiedNs &&
|
|
existing->type == entry.type;
|
|
free(name);
|
|
if (!same)
|
|
goto invalid;
|
|
}
|
|
else
|
|
{
|
|
struct RemoteNode * node = calloc(1, sizeof(*node));
|
|
if (!node)
|
|
{
|
|
free(name);
|
|
goto invalid;
|
|
}
|
|
*node = (struct RemoteNode)
|
|
{
|
|
.dataset = request->dataset,
|
|
.ino = nextInodeNL(),
|
|
.parent = request->directory->ino,
|
|
.node = entry.node,
|
|
.size = entry.size,
|
|
.createdNs = entry.createdNs,
|
|
.modifiedNs = entry.modifiedNs,
|
|
.type = entry.type,
|
|
.name = name,
|
|
.next = additions,
|
|
};
|
|
additions = node;
|
|
}
|
|
offset += (size_t)bytes64;
|
|
}
|
|
while (additions)
|
|
{
|
|
struct RemoteNode * node = additions;
|
|
additions = node->next;
|
|
node->next = files.remoteNodes;
|
|
files.remoteNodes = node;
|
|
}
|
|
request->directory->listed = true;
|
|
return true;
|
|
|
|
invalid:
|
|
while (additions)
|
|
{
|
|
struct RemoteNode * next = additions->next;
|
|
free(additions->name);
|
|
free(additions);
|
|
additions = next;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static void replyDirectory(struct RemoteRequest * request)
|
|
{
|
|
char * buffer = malloc(request->fuseSize ? request->fuseSize : 1U);
|
|
if (!buffer)
|
|
{
|
|
fuse_reply_err(request->fuseRequest, ENOMEM);
|
|
return;
|
|
}
|
|
size_t used = 0;
|
|
off_t index = 0;
|
|
struct stat attr;
|
|
if (request->fuseOffset <= index)
|
|
{
|
|
fillStat(request->directory, &attr);
|
|
const size_t entry = fuse_add_direntry(request->fuseRequest,
|
|
buffer + used, request->fuseSize - used, ".", &attr, index + 1);
|
|
if (entry > request->fuseSize - used)
|
|
goto done;
|
|
used += entry;
|
|
}
|
|
++index;
|
|
if (request->fuseOffset <= index)
|
|
{
|
|
LG_LOCK(files.lock);
|
|
struct RemoteNode * parent = remoteNodeByInodeNL(
|
|
request->directory->parent);
|
|
fillStat(parent ? parent : request->directory, &attr);
|
|
LG_UNLOCK(files.lock);
|
|
const size_t entry = fuse_add_direntry(request->fuseRequest,
|
|
buffer + used, request->fuseSize - used, "..", &attr, index + 1);
|
|
if (entry > request->fuseSize - used)
|
|
goto done;
|
|
used += entry;
|
|
}
|
|
LG_LOCK(files.lock);
|
|
for (struct RemoteNode * node = files.remoteNodes; node; node = node->next)
|
|
{
|
|
if (node->dataset != request->dataset ||
|
|
node->parent != request->directory->ino)
|
|
continue;
|
|
++index;
|
|
if (request->fuseOffset > index)
|
|
continue;
|
|
fillStat(node, &attr);
|
|
const size_t entry = fuse_add_direntry(request->fuseRequest,
|
|
buffer + used, request->fuseSize - used,
|
|
node->name, &attr, index + 1);
|
|
if (entry > request->fuseSize - used)
|
|
break;
|
|
used += entry;
|
|
}
|
|
LG_UNLOCK(files.lock);
|
|
|
|
done:
|
|
fuse_reply_buf(request->fuseRequest, buffer, used);
|
|
free(buffer);
|
|
}
|
|
|
|
static void finishRemoteRequest(struct RemoteRequest * request)
|
|
{
|
|
unregisterRemoteInterrupt(request);
|
|
bool installed = true;
|
|
if (request->request.operation == LG_CLIPBOARD_FILE_LIST)
|
|
{
|
|
LG_LOCK(files.lock);
|
|
installed = installRemoteListNL(request);
|
|
if (installed && request->reply == REMOTE_REPLY_INITIAL)
|
|
{
|
|
request->dataset->ready = true;
|
|
const bool current = request->dataset->current;
|
|
const uint64_t datasetId = request->dataset->dataset;
|
|
const uint64_t requestId = request->request.request;
|
|
LG_UNLOCK(files.lock);
|
|
if (current)
|
|
{
|
|
DEBUG_INFO("Remote clipboard file root list ready; publishing: "
|
|
"dataset=%lu, request=%lu",
|
|
datasetId, requestId);
|
|
clipboard_fileRemoteReady(datasetId);
|
|
}
|
|
completeRemoteRequest(request);
|
|
return;
|
|
}
|
|
if (installed && request->reply == REMOTE_REPLY_LOOKUP)
|
|
{
|
|
struct RemoteNode * node = remoteChildNL(request->dataset,
|
|
request->directory->ino, request->lookupName);
|
|
LG_UNLOCK(files.lock);
|
|
if (node)
|
|
replyEntry(request->fuseRequest, node);
|
|
else
|
|
fuse_reply_err(request->fuseRequest, ENOENT);
|
|
completeRemoteRequest(request);
|
|
return;
|
|
}
|
|
LG_UNLOCK(files.lock);
|
|
if (!installed && request->reply == REMOTE_REPLY_INITIAL)
|
|
{
|
|
failRemoteRequest(request, EIO);
|
|
return;
|
|
}
|
|
if (installed && request->reply == REMOTE_REPLY_READDIR)
|
|
replyDirectory(request);
|
|
else if (request->fuseRequest)
|
|
fuse_reply_err(request->fuseRequest, EIO);
|
|
}
|
|
else if (request->reply == REMOTE_REPLY_READ)
|
|
{
|
|
if (request->dataSize > request->request.length)
|
|
fuse_reply_err(request->fuseRequest, EIO);
|
|
else
|
|
fuse_reply_buf(request->fuseRequest,
|
|
(const char *)request->data, request->dataSize);
|
|
}
|
|
completeRemoteRequest(request);
|
|
}
|
|
|
|
static void interruptRemoteRequest(fuse_req_t fuseRequest, void * opaque)
|
|
{
|
|
struct RemoteRequest * request = opaque;
|
|
LG_LOCK(files.lock);
|
|
request = remoteRequestTakePointerNL(request);
|
|
const struct RemoteAction action = completeRemoteRequestNL(request);
|
|
LG_UNLOCK(files.lock);
|
|
if (!request)
|
|
return;
|
|
|
|
const LG_ClipboardFileRequest descriptor = request->request;
|
|
if (request->transportStarted &&
|
|
!clipboard_fileCancel(descriptor.dataset, descriptor.request,
|
|
LG_CLIPBOARD_FILE_ERROR_CANCELLED))
|
|
DEBUG_WARN("Failed to cancel interrupted clipboard file request");
|
|
fuse_reply_err(fuseRequest, EINTR);
|
|
freeRemoteRequest(request);
|
|
performRemoteAction(action);
|
|
}
|
|
|
|
/* Returns true when another path already took ownership of the request. */
|
|
static bool finishUnsentRemoteRequest(uint64_t requestId)
|
|
{
|
|
LG_LOCK(files.lock);
|
|
struct RemoteRequest * failed = remoteRequestTakeNL(requestId);
|
|
LG_UNLOCK(files.lock);
|
|
if (!failed)
|
|
return true;
|
|
failRemoteRequest(failed, EIO);
|
|
return false;
|
|
}
|
|
|
|
static bool requestRemote(struct RemoteDataset * dataset,
|
|
struct RemoteNode * node, LG_ClipboardFileOperation operation,
|
|
uint64_t offset, uint32_t length, enum RemoteReply reply,
|
|
fuse_req_t fuseRequest, size_t fuseSize, off_t fuseOffset,
|
|
const char * lookupName)
|
|
{
|
|
LG_LOCK(files.lock);
|
|
const bool stale = dataset->stale;
|
|
LG_UNLOCK(files.lock);
|
|
if (stale)
|
|
return false;
|
|
struct RemoteRequest * request = calloc(1, sizeof(*request));
|
|
if (!request)
|
|
return false;
|
|
if (lookupName && !(request->lookupName = strdup(lookupName)))
|
|
{
|
|
free(request);
|
|
return false;
|
|
}
|
|
LG_LOCK(files.lock);
|
|
if (dataset->stale ||
|
|
(reply == REMOTE_REPLY_INITIAL && !dataset->current))
|
|
{
|
|
LG_UNLOCK(files.lock);
|
|
freeRemoteRequest(request);
|
|
return false;
|
|
}
|
|
request->request = (LG_ClipboardFileRequest)
|
|
{
|
|
.dataset = dataset->dataset,
|
|
.request = nextRequestNL(),
|
|
.node = node->node,
|
|
.offset = offset,
|
|
.length = length,
|
|
.operation = operation,
|
|
};
|
|
request->dataset = dataset;
|
|
request->directory = node;
|
|
request->reply = reply;
|
|
request->fuseRequest = fuseRequest;
|
|
request->fuseSize = fuseSize;
|
|
request->fuseOffset = fuseOffset;
|
|
request->sizeHint = KVMFR_CLIPBOARD_SIZE_UNKNOWN;
|
|
request->next = files.remoteRequests;
|
|
files.remoteRequests = request;
|
|
++dataset->requestPins;
|
|
LG_UNLOCK(files.lock);
|
|
const LG_ClipboardFileRequest descriptor = request->request;
|
|
if (fuseRequest)
|
|
fuse_req_interrupt_func(fuseRequest, interruptRemoteRequest, request);
|
|
|
|
LG_LOCK(files.lock);
|
|
struct RemoteRequest * live = files.remoteRequests;
|
|
while (live && live != request)
|
|
live = live->next;
|
|
const bool sent = live && clipboard_fileRequest(&descriptor);
|
|
if (sent)
|
|
request->transportStarted = true;
|
|
LG_UNLOCK(files.lock);
|
|
if (!live)
|
|
return true;
|
|
if (sent)
|
|
return true;
|
|
return finishUnsentRemoteRequest(descriptor.request);
|
|
}
|
|
|
|
static struct RemoteNode * datasetRootNL(struct RemoteDataset * dataset)
|
|
{
|
|
return remoteNodeByInodeNL(dataset->ino);
|
|
}
|
|
|
|
static void fuseLookup(fuse_req_t request, fuse_ino_t parent,
|
|
const char * name)
|
|
{
|
|
LG_LOCK(files.lock);
|
|
if (parent == FUSE_ROOT_ID)
|
|
{
|
|
for (struct RemoteDataset * dataset = files.remoteDatasets;
|
|
dataset; dataset = dataset->next)
|
|
if (dataset->ready && !dataset->stale &&
|
|
!strcmp(dataset->name, name))
|
|
{
|
|
struct RemoteNode * node = datasetRootNL(dataset);
|
|
const bool pinned = pinRemoteOperationNL(dataset, true);
|
|
LG_UNLOCK(files.lock);
|
|
if (pinned)
|
|
replyEntry(request, node);
|
|
else
|
|
fuse_reply_err(request, ESTALE);
|
|
releaseRemoteOperation(pinned ? dataset : NULL);
|
|
return;
|
|
}
|
|
LG_UNLOCK(files.lock);
|
|
fuse_reply_err(request, ENOENT);
|
|
return;
|
|
}
|
|
struct RemoteNode * directory = remoteNodeByInodeNL(parent);
|
|
const int error = !directory ? ENOENT :
|
|
directory->dataset->stale ? ESTALE :
|
|
directory->type != KVMFR_CLIPBOARD_FILE_TYPE_DIRECTORY ? ENOTDIR : 0;
|
|
if (error)
|
|
{
|
|
LG_UNLOCK(files.lock);
|
|
fuse_reply_err(request, error);
|
|
return;
|
|
}
|
|
struct RemoteNode * child = remoteChildNL(directory->dataset, parent, name);
|
|
const bool listed = directory->listed;
|
|
struct RemoteDataset * dataset = directory->dataset;
|
|
const bool pinned = pinRemoteOperationNL(dataset, false);
|
|
LG_UNLOCK(files.lock);
|
|
if (!pinned)
|
|
{
|
|
fuse_reply_err(request, ESTALE);
|
|
return;
|
|
}
|
|
if (child)
|
|
{
|
|
replyEntry(request, child);
|
|
releaseRemoteOperation(dataset);
|
|
return;
|
|
}
|
|
if (listed)
|
|
{
|
|
fuse_reply_err(request, ENOENT);
|
|
releaseRemoteOperation(dataset);
|
|
return;
|
|
}
|
|
if (!requestRemote(dataset, directory,
|
|
LG_CLIPBOARD_FILE_LIST, 0, 0, REMOTE_REPLY_LOOKUP,
|
|
request, 0, 0, name))
|
|
fuse_reply_err(request, EIO);
|
|
releaseRemoteOperation(dataset);
|
|
}
|
|
|
|
static void fillRootStat(struct stat * value)
|
|
{
|
|
memset(value, 0, sizeof(*value));
|
|
value->st_ino = FUSE_ROOT_ID;
|
|
value->st_mode = S_IFDIR | 0500;
|
|
value->st_nlink = 2;
|
|
value->st_uid = getuid();
|
|
value->st_gid = getgid();
|
|
}
|
|
|
|
static void fuseGetattr(fuse_req_t request, fuse_ino_t ino,
|
|
struct fuse_file_info * info)
|
|
{
|
|
(void)info;
|
|
struct stat value;
|
|
if (ino == FUSE_ROOT_ID)
|
|
fillRootStat(&value);
|
|
else
|
|
{
|
|
LG_LOCK(files.lock);
|
|
struct RemoteNode * node = remoteNodeByInodeNL(ino);
|
|
if (node && !node->dataset->stale)
|
|
fillStat(node, &value);
|
|
const bool stale = node && node->dataset->stale;
|
|
const bool found = node && !stale;
|
|
LG_UNLOCK(files.lock);
|
|
if (!found)
|
|
{
|
|
fuse_reply_err(request, stale ? ESTALE : ENOENT);
|
|
return;
|
|
}
|
|
}
|
|
fuse_reply_attr(request, &value, FILE_ATTR_TIMEOUT);
|
|
}
|
|
|
|
static void fuseOpendir(fuse_req_t request, fuse_ino_t ino,
|
|
struct fuse_file_info * info)
|
|
{
|
|
struct RemoteDataset * dataset = NULL;
|
|
if (ino != FUSE_ROOT_ID)
|
|
{
|
|
LG_LOCK(files.lock);
|
|
struct RemoteNode * node = remoteNodeByInodeNL(ino);
|
|
const int error = !node ? ENOENT :
|
|
node->dataset->stale ? ESTALE :
|
|
node->type != KVMFR_CLIPBOARD_FILE_TYPE_DIRECTORY ? ENOTDIR : 0;
|
|
if (!error)
|
|
{
|
|
dataset = node->dataset;
|
|
++dataset->openPins;
|
|
info->fh = dataset->acquisition;
|
|
}
|
|
LG_UNLOCK(files.lock);
|
|
if (error)
|
|
{
|
|
fuse_reply_err(request, error);
|
|
return;
|
|
}
|
|
}
|
|
if (fuse_reply_open(request, info) == 0 || !dataset)
|
|
return;
|
|
LG_LOCK(files.lock);
|
|
if (dataset->openPins)
|
|
--dataset->openPins;
|
|
const struct RemoteAction action = retireRemoteDatasetNL(dataset);
|
|
LG_UNLOCK(files.lock);
|
|
performRemoteAction(action);
|
|
}
|
|
|
|
static void replyRootDirectory(fuse_req_t request, size_t size, off_t offset)
|
|
{
|
|
char * buffer = malloc(size ? size : 1U);
|
|
if (!buffer)
|
|
{
|
|
fuse_reply_err(request, ENOMEM);
|
|
return;
|
|
}
|
|
size_t used = 0;
|
|
off_t index = 0;
|
|
struct stat attr;
|
|
fillRootStat(&attr);
|
|
const char * fixed[] = { ".", ".." };
|
|
for (unsigned i = 0; i < 2; ++i, ++index)
|
|
{
|
|
if (offset > index)
|
|
continue;
|
|
const size_t entry = fuse_add_direntry(request, buffer + used,
|
|
size - used, fixed[i], &attr, index + 1);
|
|
if (entry > size - used)
|
|
goto done;
|
|
used += entry;
|
|
}
|
|
LG_LOCK(files.lock);
|
|
for (struct RemoteDataset * dataset = files.remoteDatasets;
|
|
dataset; dataset = dataset->next)
|
|
{
|
|
if (!dataset->ready || dataset->stale)
|
|
continue;
|
|
++index;
|
|
if (offset > index)
|
|
continue;
|
|
struct RemoteNode * node = datasetRootNL(dataset);
|
|
fillStat(node, &attr);
|
|
const size_t entry = fuse_add_direntry(request, buffer + used,
|
|
size - used, dataset->name, &attr, index + 1);
|
|
if (entry > size - used)
|
|
break;
|
|
used += entry;
|
|
}
|
|
LG_UNLOCK(files.lock);
|
|
|
|
done:
|
|
fuse_reply_buf(request, buffer, used);
|
|
free(buffer);
|
|
}
|
|
|
|
static void fuseReaddir(fuse_req_t request, fuse_ino_t ino,
|
|
size_t size, off_t offset, struct fuse_file_info * info)
|
|
{
|
|
if (ino == FUSE_ROOT_ID)
|
|
{
|
|
replyRootDirectory(request, size, offset);
|
|
return;
|
|
}
|
|
LG_LOCK(files.lock);
|
|
struct RemoteNode * directory = remoteNodeByInodeNL(ino);
|
|
const int error = !directory ? ENOENT :
|
|
directory->dataset->stale ||
|
|
info->fh != directory->dataset->acquisition ? ESTALE :
|
|
directory->type != KVMFR_CLIPBOARD_FILE_TYPE_DIRECTORY ? ENOTDIR : 0;
|
|
if (error)
|
|
{
|
|
LG_UNLOCK(files.lock);
|
|
fuse_reply_err(request, error);
|
|
return;
|
|
}
|
|
const bool listed = directory->listed;
|
|
struct RemoteDataset * dataset = directory->dataset;
|
|
const bool pinned = pinRemoteOperationNL(dataset, false);
|
|
LG_UNLOCK(files.lock);
|
|
if (!pinned)
|
|
{
|
|
fuse_reply_err(request, ESTALE);
|
|
return;
|
|
}
|
|
if (listed)
|
|
{
|
|
struct RemoteRequest local =
|
|
{
|
|
.dataset = dataset,
|
|
.directory = directory,
|
|
.fuseRequest = request,
|
|
.fuseSize = size,
|
|
.fuseOffset = offset,
|
|
};
|
|
replyDirectory(&local);
|
|
releaseRemoteOperation(dataset);
|
|
return;
|
|
}
|
|
if (!requestRemote(dataset, directory,
|
|
LG_CLIPBOARD_FILE_LIST, 0, 0, REMOTE_REPLY_READDIR,
|
|
request, size, offset, NULL))
|
|
fuse_reply_err(request, EIO);
|
|
releaseRemoteOperation(dataset);
|
|
}
|
|
|
|
static void fuseOpen(fuse_req_t request, fuse_ino_t ino,
|
|
struct fuse_file_info * info)
|
|
{
|
|
LG_LOCK(files.lock);
|
|
struct RemoteNode * node = remoteNodeByInodeNL(ino);
|
|
const int error = !node ? ENOENT :
|
|
node->dataset->stale ? ESTALE :
|
|
node->type == KVMFR_CLIPBOARD_FILE_TYPE_DIRECTORY ? EISDIR : 0;
|
|
if (error)
|
|
{
|
|
LG_UNLOCK(files.lock);
|
|
fuse_reply_err(request, error);
|
|
return;
|
|
}
|
|
if ((info->flags & O_ACCMODE) != O_RDONLY)
|
|
{
|
|
LG_UNLOCK(files.lock);
|
|
fuse_reply_err(request, EROFS);
|
|
return;
|
|
}
|
|
struct RemoteDataset * dataset = node->dataset;
|
|
++dataset->openPins;
|
|
info->fh = dataset->acquisition;
|
|
LG_UNLOCK(files.lock);
|
|
info->keep_cache = 1;
|
|
if (fuse_reply_open(request, info) == 0)
|
|
return;
|
|
LG_LOCK(files.lock);
|
|
if (dataset->openPins)
|
|
--dataset->openPins;
|
|
const struct RemoteAction action = retireRemoteDatasetNL(dataset);
|
|
LG_UNLOCK(files.lock);
|
|
performRemoteAction(action);
|
|
}
|
|
|
|
static void fuseRead(fuse_req_t request, fuse_ino_t ino, size_t size,
|
|
off_t offset, struct fuse_file_info * info)
|
|
{
|
|
if (offset < 0)
|
|
{
|
|
fuse_reply_err(request, EINVAL);
|
|
return;
|
|
}
|
|
LG_LOCK(files.lock);
|
|
struct RemoteNode * node = remoteNodeByInodeNL(ino);
|
|
const int error = !node ? ENOENT :
|
|
node->dataset->stale ||
|
|
info->fh != node->dataset->acquisition ? ESTALE :
|
|
node->type != KVMFR_CLIPBOARD_FILE_TYPE_REGULAR ? EISDIR : 0;
|
|
if (error)
|
|
{
|
|
LG_UNLOCK(files.lock);
|
|
fuse_reply_err(request, error);
|
|
return;
|
|
}
|
|
const uint64_t fileSize = node->size;
|
|
struct RemoteDataset * dataset = node->dataset;
|
|
const bool pinned = pinRemoteOperationNL(dataset, false);
|
|
LG_UNLOCK(files.lock);
|
|
if (!pinned)
|
|
{
|
|
fuse_reply_err(request, ESTALE);
|
|
return;
|
|
}
|
|
if ((uint64_t)offset >= fileSize || !size)
|
|
{
|
|
fuse_reply_buf(request, NULL, 0);
|
|
releaseRemoteOperation(dataset);
|
|
return;
|
|
}
|
|
uint64_t wanted = fileSize - (uint64_t)offset;
|
|
if (wanted > size)
|
|
wanted = size;
|
|
if (wanted > FILE_REQUEST_MAX)
|
|
wanted = FILE_REQUEST_MAX;
|
|
if (!requestRemote(dataset, node, LG_CLIPBOARD_FILE_READ,
|
|
(uint64_t)offset, (uint32_t)wanted, REMOTE_REPLY_READ,
|
|
request, 0, 0, NULL))
|
|
fuse_reply_err(request, EIO);
|
|
releaseRemoteOperation(dataset);
|
|
}
|
|
|
|
static void fuseStatfs(fuse_req_t request, fuse_ino_t ino)
|
|
{
|
|
(void)ino;
|
|
struct statvfs value;
|
|
memset(&value, 0, sizeof(value));
|
|
value.f_bsize = 4096;
|
|
value.f_frsize = 4096;
|
|
value.f_namemax = NAME_MAX;
|
|
value.f_flag = ST_RDONLY | ST_NOSUID;
|
|
fuse_reply_statfs(request, &value);
|
|
}
|
|
|
|
static void fuseAccess(fuse_req_t request, fuse_ino_t ino, int mask)
|
|
{
|
|
if (mask & W_OK)
|
|
{
|
|
fuse_reply_err(request, EROFS);
|
|
return;
|
|
}
|
|
if (ino == FUSE_ROOT_ID)
|
|
{
|
|
fuse_reply_err(request, 0);
|
|
return;
|
|
}
|
|
LG_LOCK(files.lock);
|
|
struct RemoteNode * node = remoteNodeByInodeNL(ino);
|
|
const bool found = node && !node->dataset->stale;
|
|
const bool stale = node && node->dataset->stale;
|
|
LG_UNLOCK(files.lock);
|
|
fuse_reply_err(request, found ? 0 : stale ? ESTALE : ENOENT);
|
|
}
|
|
|
|
static void forgetRemoteNode(fuse_ino_t ino, uint64_t count)
|
|
{
|
|
LG_LOCK(files.lock);
|
|
struct RemoteNode * node = remoteNodeByInodeNL(ino);
|
|
struct RemoteAction action = { 0 };
|
|
if (node)
|
|
{
|
|
if (count > node->lookups)
|
|
count = node->lookups;
|
|
node->lookups -= count;
|
|
if (count > node->dataset->lookupPins)
|
|
node->dataset->lookupPins = 0;
|
|
else
|
|
node->dataset->lookupPins -= count;
|
|
action = retireRemoteDatasetNL(node->dataset);
|
|
}
|
|
LG_UNLOCK(files.lock);
|
|
performRemoteAction(action);
|
|
}
|
|
|
|
static void fuseForget(fuse_req_t request, fuse_ino_t ino,
|
|
uint64_t count)
|
|
{
|
|
if (ino != FUSE_ROOT_ID)
|
|
forgetRemoteNode(ino, count);
|
|
fuse_reply_none(request);
|
|
}
|
|
|
|
static void fuseForgetMulti(fuse_req_t request, size_t count,
|
|
struct fuse_forget_data * forgets)
|
|
{
|
|
for (size_t i = 0; i < count; ++i)
|
|
if (forgets[i].ino != FUSE_ROOT_ID)
|
|
forgetRemoteNode(forgets[i].ino, forgets[i].nlookup);
|
|
fuse_reply_none(request);
|
|
}
|
|
|
|
static void releaseRemoteHandle(fuse_req_t request,
|
|
struct fuse_file_info * info)
|
|
{
|
|
if (!info->fh)
|
|
{
|
|
fuse_reply_err(request, 0);
|
|
return;
|
|
}
|
|
LG_LOCK(files.lock);
|
|
struct RemoteDataset * dataset = remoteDatasetByAcquisitionNL(info->fh);
|
|
const bool found = dataset != NULL;
|
|
if (dataset && dataset->openPins)
|
|
--dataset->openPins;
|
|
const struct RemoteAction action = retireRemoteDatasetNL(dataset);
|
|
LG_UNLOCK(files.lock);
|
|
fuse_reply_err(request, found ? 0 : ESTALE);
|
|
performRemoteAction(action);
|
|
}
|
|
|
|
static void fuseRelease(fuse_req_t request, fuse_ino_t ino,
|
|
struct fuse_file_info * info)
|
|
{
|
|
(void)ino;
|
|
releaseRemoteHandle(request, info);
|
|
}
|
|
|
|
static void fuseReleasedir(fuse_req_t request, fuse_ino_t ino,
|
|
struct fuse_file_info * info)
|
|
{
|
|
(void)ino;
|
|
releaseRemoteHandle(request, info);
|
|
}
|
|
|
|
static void fuseInit(void * userData, struct fuse_conn_info * connection)
|
|
{
|
|
(void)userData;
|
|
connection->max_read = FILE_REQUEST_MAX;
|
|
}
|
|
|
|
static const struct fuse_lowlevel_ops fuseOps =
|
|
{
|
|
.init = fuseInit,
|
|
.lookup = fuseLookup,
|
|
.forget = fuseForget,
|
|
.forget_multi = fuseForgetMulti,
|
|
.getattr = fuseGetattr,
|
|
.opendir = fuseOpendir,
|
|
.readdir = fuseReaddir,
|
|
.releasedir = fuseReleasedir,
|
|
.open = fuseOpen,
|
|
.read = fuseRead,
|
|
.release = fuseRelease,
|
|
.access = fuseAccess,
|
|
.statfs = fuseStatfs,
|
|
};
|
|
|
|
enum FuseWaitResult
|
|
{
|
|
FUSE_WAIT_ERROR,
|
|
FUSE_WAIT_SESSION,
|
|
FUSE_WAIT_WAKE,
|
|
FUSE_WAIT_TIMEOUT,
|
|
};
|
|
|
|
static enum FuseWaitResult waitFuseEvent(
|
|
int sessionFd, int stopFd, int timeout)
|
|
{
|
|
struct pollfd descriptors[] =
|
|
{
|
|
{ .fd = sessionFd, .events = POLLIN },
|
|
{ .fd = stopFd, .events = POLLIN },
|
|
};
|
|
int result;
|
|
do
|
|
result = poll(descriptors,
|
|
sizeof(descriptors) / sizeof(*descriptors), timeout);
|
|
while (result < 0 && errno == EINTR);
|
|
if (result < 0)
|
|
return FUSE_WAIT_ERROR;
|
|
if (!result)
|
|
return FUSE_WAIT_TIMEOUT;
|
|
if (descriptors[1].revents & POLLIN)
|
|
{
|
|
uint64_t value;
|
|
ssize_t received;
|
|
do
|
|
received = read(stopFd, &value, sizeof(value));
|
|
while (received < 0 && errno == EINTR);
|
|
if (received != (ssize_t)sizeof(value) &&
|
|
!(received < 0 && errno == EAGAIN))
|
|
return FUSE_WAIT_ERROR;
|
|
return FUSE_WAIT_WAKE;
|
|
}
|
|
if (descriptors[1].revents)
|
|
{
|
|
errno = EIO;
|
|
return FUSE_WAIT_ERROR;
|
|
}
|
|
if (descriptors[0].revents & POLLIN)
|
|
return FUSE_WAIT_SESSION;
|
|
errno = EIO;
|
|
return FUSE_WAIT_ERROR;
|
|
}
|
|
|
|
static bool wakeFuseThread(int stopFd)
|
|
{
|
|
const uint64_t value = 1;
|
|
ssize_t result;
|
|
do
|
|
result = write(stopFd, &value, sizeof(value));
|
|
while (result < 0 && errno == EINTR);
|
|
return result == (ssize_t)sizeof(value) ||
|
|
(result < 0 && errno == EAGAIN);
|
|
}
|
|
|
|
static int fuseThread(void * opaque)
|
|
{
|
|
struct fuse_session * session = opaque;
|
|
struct fuse_buf buffer =
|
|
{
|
|
.mem = NULL,
|
|
};
|
|
int result = 0;
|
|
while (!fuse_session_exited(session))
|
|
{
|
|
switch (waitFuseEvent(fuse_session_fd(session), files.fuseStopFd,
|
|
nextRemoteDeliveryTimeout()))
|
|
{
|
|
case FUSE_WAIT_WAKE:
|
|
{
|
|
LG_LOCK(files.lock);
|
|
const bool stopping = files.stopping;
|
|
LG_UNLOCK(files.lock);
|
|
if (stopping)
|
|
goto done;
|
|
reapRemoteDeliveries(false);
|
|
continue;
|
|
}
|
|
|
|
case FUSE_WAIT_TIMEOUT:
|
|
reapRemoteDeliveries(false);
|
|
continue;
|
|
|
|
case FUSE_WAIT_ERROR:
|
|
result = -errno;
|
|
goto done;
|
|
|
|
case FUSE_WAIT_SESSION:
|
|
break;
|
|
}
|
|
|
|
result = fuse_session_receive_buf(session, &buffer);
|
|
if (result == -EINTR)
|
|
continue;
|
|
if (result <= 0)
|
|
goto done;
|
|
fuse_session_process_buf(session, &buffer);
|
|
}
|
|
|
|
done:
|
|
free(buffer.mem);
|
|
if (result > 0)
|
|
result = 0;
|
|
fuse_session_reset(session);
|
|
return result;
|
|
}
|
|
|
|
static char * uriDecode(const char * value, size_t length)
|
|
{
|
|
static const char hex[] = "0123456789abcdef";
|
|
char * result = malloc(length + 1U);
|
|
if (!result)
|
|
return NULL;
|
|
size_t out = 0;
|
|
for (size_t i = 0; i < length; ++i)
|
|
{
|
|
unsigned char ch = (unsigned char)value[i];
|
|
if (ch == '%')
|
|
{
|
|
if (i + 2U >= length)
|
|
goto invalid;
|
|
const char * high = strchr(hex, (char)(value[i + 1U] | 0x20));
|
|
const char * low = strchr(hex, (char)(value[i + 2U] | 0x20));
|
|
if (!high || !low)
|
|
goto invalid;
|
|
ch = (unsigned char)(((high - hex) << 4) | (low - hex));
|
|
i += 2U;
|
|
}
|
|
if (!ch)
|
|
goto invalid;
|
|
result[out++] = (char)ch;
|
|
}
|
|
result[out] = 0;
|
|
return result;
|
|
|
|
invalid:
|
|
free(result);
|
|
return NULL;
|
|
}
|
|
|
|
static bool localNodeStat(struct LocalNode * node, const struct stat * value)
|
|
{
|
|
if (S_ISREG(value->st_mode))
|
|
node->type = KVMFR_CLIPBOARD_FILE_TYPE_REGULAR;
|
|
else if (S_ISDIR(value->st_mode))
|
|
node->type = KVMFR_CLIPBOARD_FILE_TYPE_DIRECTORY;
|
|
else
|
|
return false;
|
|
node->size = value->st_size < 0 ? 0 : (uint64_t)value->st_size;
|
|
node->modifiedNs = nsFromTimespec(value->st_mtim);
|
|
node->createdNs = 0;
|
|
node->device = value->st_dev;
|
|
node->inode = value->st_ino;
|
|
return true;
|
|
}
|
|
|
|
static bool localStatSupported(const struct stat * value)
|
|
{
|
|
return S_ISREG(value->st_mode) || S_ISDIR(value->st_mode);
|
|
}
|
|
|
|
static bool localStatEqual(const struct stat * first,
|
|
const struct stat * second)
|
|
{
|
|
return first->st_dev == second->st_dev &&
|
|
first->st_ino == second->st_ino &&
|
|
((S_ISREG(first->st_mode) && S_ISREG(second->st_mode)) ||
|
|
(S_ISDIR(first->st_mode) && S_ISDIR(second->st_mode))) &&
|
|
first->st_size == second->st_size &&
|
|
first->st_mtim.tv_sec == second->st_mtim.tv_sec &&
|
|
first->st_mtim.tv_nsec == second->st_mtim.tv_nsec;
|
|
}
|
|
|
|
static bool localNodeMatches(const struct LocalNode * node,
|
|
const struct stat * value)
|
|
{
|
|
return node->device == value->st_dev && node->inode == value->st_ino &&
|
|
((node->type == KVMFR_CLIPBOARD_FILE_TYPE_REGULAR &&
|
|
S_ISREG(value->st_mode)) ||
|
|
(node->type == KVMFR_CLIPBOARD_FILE_TYPE_DIRECTORY &&
|
|
S_ISDIR(value->st_mode))) &&
|
|
node->size == (value->st_size < 0 ? 0 : (uint64_t)value->st_size) &&
|
|
node->modifiedNs == nsFromTimespec(value->st_mtim);
|
|
}
|
|
|
|
static int openAbsoluteLocal(const char * path, char ** name,
|
|
struct stat * value)
|
|
{
|
|
if (!path || path[0] != '/' || !path[1])
|
|
return -1;
|
|
|
|
int current = open("/", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
|
|
if (current < 0)
|
|
return -1;
|
|
|
|
const char * component = path + 1;
|
|
for (;;)
|
|
{
|
|
const char * slash = strchr(component, '/');
|
|
const size_t length = slash ? (size_t)(slash - component) :
|
|
strlen(component);
|
|
if (!length || length > NAME_MAX ||
|
|
(length == 1U && component[0] == '.') ||
|
|
(length == 2U && component[0] == '.' && component[1] == '.'))
|
|
goto failed;
|
|
|
|
char * part = strndup(component, length);
|
|
if (!part)
|
|
goto failed;
|
|
|
|
struct stat before;
|
|
const bool final = !slash;
|
|
int next = -1;
|
|
if (fstatat(current, part, &before, AT_SYMLINK_NOFOLLOW) == 0 &&
|
|
!S_ISLNK(before.st_mode) &&
|
|
(final ? localStatSupported(&before) : S_ISDIR(before.st_mode)))
|
|
{
|
|
int flags = O_RDONLY | O_CLOEXEC | O_NOFOLLOW | O_NONBLOCK;
|
|
if (!final || S_ISDIR(before.st_mode))
|
|
flags |= O_DIRECTORY;
|
|
next = openat(current, part, flags);
|
|
}
|
|
if (next < 0)
|
|
{
|
|
free(part);
|
|
goto failed;
|
|
}
|
|
|
|
struct stat after;
|
|
if (fstat(next, &after) < 0 || !localStatEqual(&before, &after))
|
|
{
|
|
close(next);
|
|
free(part);
|
|
goto failed;
|
|
}
|
|
close(current);
|
|
current = next;
|
|
|
|
if (final)
|
|
{
|
|
*name = part;
|
|
*value = after;
|
|
return current;
|
|
}
|
|
free(part);
|
|
component = slash + 1;
|
|
}
|
|
|
|
failed:
|
|
close(current);
|
|
return -1;
|
|
}
|
|
|
|
static int openLocalNode(const struct LocalNode * node,
|
|
struct stat * value)
|
|
{
|
|
if (!node || !node->root || node->root->fd < 0)
|
|
return -1;
|
|
|
|
int current;
|
|
if (node->root->type == KVMFR_CLIPBOARD_FILE_TYPE_DIRECTORY)
|
|
current = openat(node->root->fd, ".",
|
|
O_RDONLY | O_DIRECTORY | O_CLOEXEC | O_NOFOLLOW);
|
|
else
|
|
current = fcntl(node->root->fd, F_DUPFD_CLOEXEC, 0);
|
|
if (current < 0)
|
|
return -1;
|
|
|
|
const char * component = node->path;
|
|
while (*component)
|
|
{
|
|
const char * slash = strchr(component, '/');
|
|
const size_t length = slash ? (size_t)(slash - component) :
|
|
strlen(component);
|
|
if (!length || length > NAME_MAX ||
|
|
(length == 1U && component[0] == '.') ||
|
|
(length == 2U && component[0] == '.' && component[1] == '.'))
|
|
goto failed;
|
|
char * part = strndup(component, length);
|
|
if (!part)
|
|
goto failed;
|
|
int flags = O_RDONLY | O_CLOEXEC | O_NOFOLLOW | O_NONBLOCK;
|
|
if (slash || node->type == KVMFR_CLIPBOARD_FILE_TYPE_DIRECTORY)
|
|
flags |= O_DIRECTORY;
|
|
const int next = openat(current, part, flags);
|
|
free(part);
|
|
if (next < 0)
|
|
goto failed;
|
|
close(current);
|
|
current = next;
|
|
component = slash ? slash + 1 : component + length;
|
|
}
|
|
|
|
if (fstat(current, value) < 0)
|
|
goto failed;
|
|
if (!localNodeMatches(node, value))
|
|
{
|
|
errno = ESTALE;
|
|
goto failed;
|
|
}
|
|
return current;
|
|
|
|
failed:
|
|
close(current);
|
|
return -1;
|
|
}
|
|
|
|
static void freeLocalDataset(struct LocalDataset * dataset)
|
|
{
|
|
if (!dataset)
|
|
return;
|
|
struct LocalNode * node = dataset->nodes;
|
|
while (node)
|
|
{
|
|
struct LocalNode * next = node->next;
|
|
if (node->fd >= 0)
|
|
close(node->fd);
|
|
free(node->path);
|
|
free(node->name);
|
|
free(node);
|
|
node = next;
|
|
}
|
|
free(dataset);
|
|
}
|
|
|
|
static struct LocalDataset * takeUnusedLocalDatasetNL(
|
|
struct LocalDataset * dataset)
|
|
{
|
|
if (!dataset || dataset->current || dataset->references)
|
|
return NULL;
|
|
struct LocalDataset ** link = &files.localDatasets;
|
|
while (*link && *link != dataset)
|
|
link = &(*link)->next;
|
|
if (!*link)
|
|
return NULL;
|
|
*link = dataset->next;
|
|
dataset->next = NULL;
|
|
return dataset;
|
|
}
|
|
|
|
static char * uriPath(const char * line, size_t length)
|
|
{
|
|
if (length < strlen(FILE_URI_PREFIX) ||
|
|
strncasecmp(line, FILE_URI_PREFIX, strlen(FILE_URI_PREFIX)))
|
|
return NULL;
|
|
line += strlen(FILE_URI_PREFIX);
|
|
length -= strlen(FILE_URI_PREFIX);
|
|
|
|
if (length >= 10U && !strncasecmp(line, "localhost/", 10U))
|
|
{
|
|
line += 9U;
|
|
length -= 9U;
|
|
}
|
|
else if (!length || line[0] != '/')
|
|
return NULL;
|
|
|
|
static const char punctuation[] = "-._~!$&'()*+,;=:@/%";
|
|
for (size_t i = 0; i < length; ++i)
|
|
{
|
|
const unsigned char ch = (unsigned char)line[i];
|
|
if (!((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') ||
|
|
(ch >= '0' && ch <= '9') || strchr(punctuation, ch)))
|
|
return NULL;
|
|
}
|
|
char * path = uriDecode(line, length);
|
|
if (!path)
|
|
return NULL;
|
|
size_t pathLength = strlen(path);
|
|
while (pathLength > 1U && path[pathLength - 1U] == '/')
|
|
path[--pathLength] = 0;
|
|
return path;
|
|
}
|
|
|
|
static struct LocalNode * localNodeByIdNL(struct LocalDataset * dataset,
|
|
uint64_t node)
|
|
{
|
|
for (struct LocalNode * item = dataset->nodes; item; item = item->next)
|
|
if (item->node == node)
|
|
return item;
|
|
return NULL;
|
|
}
|
|
|
|
static struct LocalNode * localChildNL(struct LocalDataset * dataset,
|
|
uint64_t parent, const char * name)
|
|
{
|
|
for (struct LocalNode * item = dataset->nodes; item; item = item->next)
|
|
if (item->parent == parent && !strcmp(item->name, name))
|
|
return item;
|
|
return NULL;
|
|
}
|
|
|
|
static struct LocalNode * addLocalNode(struct LocalDataset * dataset,
|
|
uint64_t parent, struct LocalNode * root, char * path,
|
|
const char * name, const struct stat * value)
|
|
{
|
|
struct LocalNode * node = calloc(1, sizeof(*node));
|
|
if (!node)
|
|
{
|
|
free(path);
|
|
return NULL;
|
|
}
|
|
node->fd = -1;
|
|
node->path = path;
|
|
node->name = strdup(name);
|
|
if (!node->name || !localNodeStat(node, value))
|
|
{
|
|
free(node->path);
|
|
free(node->name);
|
|
free(node);
|
|
return NULL;
|
|
}
|
|
node->dataset = dataset;
|
|
node->root = root ? root : node;
|
|
node->parent = parent;
|
|
node->node = ++dataset->nodeSerial;
|
|
node->next = dataset->nodes;
|
|
dataset->nodes = node;
|
|
return node;
|
|
}
|
|
|
|
bool clipboardFiles_setLocal(const char * mime,
|
|
const void * data, size_t size)
|
|
{
|
|
if (!mime || (!data && size))
|
|
return false;
|
|
|
|
const bool gnome_or_mate =
|
|
!strcmp(mime, FILE_GNOME_MIME) ||
|
|
!strcmp(mime, FILE_MATE_MIME );
|
|
|
|
if (strcmp(mime, FILE_URI_MIME) && !gnome_or_mate)
|
|
return false;
|
|
|
|
const char * text = data;
|
|
size_t offset = 0;
|
|
if (gnome_or_mate)
|
|
{
|
|
const char * newline = size ? memchr(text, '\n', size) : NULL;
|
|
if (!newline)
|
|
return false;
|
|
const size_t operationLength = (size_t)(newline - text);
|
|
if ((operationLength != 4U || memcmp(text, "copy", 4U)) &&
|
|
(operationLength != 3U || memcmp(text, "cut", 3U)))
|
|
return false;
|
|
offset = operationLength + 1U;
|
|
}
|
|
struct LocalDataset * dataset = calloc(1, sizeof(*dataset));
|
|
if (!dataset)
|
|
return false;
|
|
while (offset < size)
|
|
{
|
|
const char * line = text + offset;
|
|
const char * newline = memchr(line, '\n', size - offset);
|
|
size_t length = newline ? (size_t)(newline - line) : size - offset;
|
|
if (length && line[length - 1U] == '\r')
|
|
--length;
|
|
offset += (newline ? (size_t)(newline - line) + 1U : size - offset);
|
|
if (!length || line[0] == '#')
|
|
continue;
|
|
char * path = uriPath(line, length);
|
|
if (!path)
|
|
goto invalid;
|
|
char * name = NULL;
|
|
struct stat value;
|
|
const int fd = openAbsoluteLocal(path, &name, &value);
|
|
free(path);
|
|
if (fd < 0)
|
|
{
|
|
free(name);
|
|
goto invalid;
|
|
}
|
|
char * relative = strdup("");
|
|
if (!relative || !validUtf8((const uint8_t *)name, strlen(name)) ||
|
|
localChildNL(dataset, KVMFR_CLIPBOARD_FILE_ROOT_NODE, name))
|
|
{
|
|
close(fd);
|
|
free(relative);
|
|
free(name);
|
|
goto invalid;
|
|
}
|
|
struct LocalNode * node = addLocalNode(dataset,
|
|
KVMFR_CLIPBOARD_FILE_ROOT_NODE, NULL, relative, name, &value);
|
|
free(name);
|
|
if (!node)
|
|
{
|
|
close(fd);
|
|
goto invalid;
|
|
}
|
|
node->fd = fd;
|
|
}
|
|
if (!dataset->nodes)
|
|
goto invalid;
|
|
dataset->current = true;
|
|
LG_LOCK(files.lock);
|
|
dataset->dataset = nextDatasetNL();
|
|
struct LocalDataset * retired = NULL;
|
|
if (files.localCurrent)
|
|
{
|
|
files.localCurrent->current = false;
|
|
retired = takeUnusedLocalDatasetNL(files.localCurrent);
|
|
}
|
|
dataset->next = files.localDatasets;
|
|
files.localDatasets = dataset;
|
|
files.localCurrent = dataset;
|
|
const uint64_t datasetId = dataset->dataset;
|
|
LG_UNLOCK(files.lock);
|
|
freeLocalDataset(retired);
|
|
clipboard_notifyFiles(datasetId);
|
|
return true;
|
|
|
|
invalid:
|
|
freeLocalDataset(dataset);
|
|
return false;
|
|
}
|
|
|
|
void clipboardFiles_clearLocal(void)
|
|
{
|
|
LG_LOCK(files.lock);
|
|
struct LocalDataset * retired = NULL;
|
|
if (files.localCurrent)
|
|
{
|
|
files.localCurrent->current = false;
|
|
retired = takeUnusedLocalDatasetNL(files.localCurrent);
|
|
}
|
|
files.localCurrent = NULL;
|
|
LG_UNLOCK(files.lock);
|
|
freeLocalDataset(retired);
|
|
}
|
|
|
|
static char * uriEncodePath(const char * path)
|
|
{
|
|
static const char hex[] = "0123456789ABCDEF";
|
|
const size_t length = strlen(path);
|
|
if (length > (SIZE_MAX - 1U) / 3U)
|
|
return NULL;
|
|
char * result = malloc(length * 3U + 1U);
|
|
if (!result)
|
|
return NULL;
|
|
size_t out = 0;
|
|
for (size_t i = 0; i < length; ++i)
|
|
{
|
|
const unsigned char ch = (unsigned char)path[i];
|
|
if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') ||
|
|
(ch >= '0' && ch <= '9') || ch == '/' || ch == '-' ||
|
|
ch == '_' || ch == '.' || ch == '~')
|
|
result[out++] = (char)ch;
|
|
else
|
|
{
|
|
result[out++] = '%';
|
|
result[out++] = hex[ch >> 4];
|
|
result[out++] = hex[ch & 0xfU];
|
|
}
|
|
}
|
|
result[out] = 0;
|
|
return result;
|
|
}
|
|
|
|
static bool getRemoteDatasetNL(struct RemoteDataset * dataset,
|
|
const char * mime, char ** data, size_t * size)
|
|
{
|
|
if (!strcmp(mime, FILE_KDE_CUT_MIME))
|
|
{
|
|
*data = strdup("0");
|
|
*size = *data ? 1U : 0U;
|
|
return *data != NULL;
|
|
}
|
|
|
|
const bool gnome_or_mate =
|
|
!strcmp(mime, FILE_GNOME_MIME) ||
|
|
!strcmp(mime, FILE_MATE_MIME );
|
|
|
|
if (strcmp(mime, FILE_URI_MIME) && !gnome_or_mate)
|
|
return false;
|
|
|
|
char * result = NULL;
|
|
size_t used = 0;
|
|
size_t capacity = 0;
|
|
if (gnome_or_mate)
|
|
{
|
|
if (!grow((void **)&result, &capacity, 5U))
|
|
return false;
|
|
memcpy(result, "copy", 4U);
|
|
used = 4U;
|
|
}
|
|
for (struct RemoteNode * node = files.remoteNodes; node; node = node->next)
|
|
{
|
|
if (node->dataset != dataset || node->parent != dataset->ino)
|
|
continue;
|
|
const size_t rawLength = strlen(files.mountpoint) + 1U +
|
|
strlen(dataset->name) + 1U + strlen(node->name) + 1U;
|
|
char * raw = malloc(rawLength);
|
|
if (!raw)
|
|
goto error;
|
|
snprintf(raw, rawLength, "%s/%s/%s",
|
|
files.mountpoint, dataset->name, node->name);
|
|
char * encoded = uriEncodePath(raw);
|
|
free(raw);
|
|
if (!encoded)
|
|
goto error;
|
|
|
|
const size_t line = strlen(FILE_URI_PREFIX) + strlen(encoded) + 2U;
|
|
if (!grow((void **)&result, &capacity, used + line + 1U))
|
|
{
|
|
free(encoded);
|
|
goto error;
|
|
}
|
|
|
|
if (gnome_or_mate)
|
|
used += (size_t)snprintf(result + used, capacity - used,
|
|
"\n" FILE_URI_PREFIX "%s", encoded);
|
|
else
|
|
used += (size_t)snprintf(result + used, capacity - used,
|
|
FILE_URI_PREFIX "%s\r\n", encoded);
|
|
free(encoded);
|
|
}
|
|
if (!result)
|
|
return false;
|
|
*data = result;
|
|
*size = used;
|
|
return true;
|
|
|
|
error:
|
|
free(result);
|
|
return false;
|
|
}
|
|
|
|
bool clipboardFiles_getRemote(const char * mime,
|
|
char ** data, size_t * size)
|
|
{
|
|
if (!mime || !data || !size)
|
|
return false;
|
|
*data = NULL;
|
|
*size = 0;
|
|
LG_LOCK(files.lock);
|
|
struct RemoteDataset * dataset = files.remoteCurrent;
|
|
const bool result = dataset && dataset->ready && !dataset->stale &&
|
|
getRemoteDatasetNL(dataset, mime, data, size);
|
|
LG_UNLOCK(files.lock);
|
|
return result;
|
|
}
|
|
|
|
uint64_t clipboardFiles_remotePresentationAcquire(void)
|
|
{
|
|
LG_LOCK(files.lock);
|
|
struct RemoteDataset * dataset = files.remoteCurrent;
|
|
const uint64_t presentation = dataset && dataset->ready &&
|
|
dataset->acquired && !dataset->stale ? dataset->acquisition : 0;
|
|
if (presentation)
|
|
++dataset->presentationPins;
|
|
LG_UNLOCK(files.lock);
|
|
return presentation;
|
|
}
|
|
|
|
bool clipboardFiles_getRemotePresentation(uint64_t presentation,
|
|
const char * mime, char ** data, size_t * size)
|
|
{
|
|
if (!presentation || !mime || !data || !size)
|
|
return false;
|
|
*data = NULL;
|
|
*size = 0;
|
|
LG_LOCK(files.lock);
|
|
struct RemoteDataset * dataset = remoteDatasetByAcquisitionNL(presentation);
|
|
const bool result = dataset && dataset->presentationPins &&
|
|
dataset->ready && !dataset->stale &&
|
|
getRemoteDatasetNL(dataset, mime, data, size);
|
|
LG_UNLOCK(files.lock);
|
|
return result;
|
|
}
|
|
|
|
void clipboardFiles_remotePresentationDelivered(uint64_t presentation)
|
|
{
|
|
if (!presentation)
|
|
return;
|
|
LG_LOCK(files.lock);
|
|
struct RemoteDataset * dataset =
|
|
remoteDatasetByAcquisitionNL(presentation);
|
|
const bool delivered = dataset && dataset->presentationPins &&
|
|
dataset->ready && dataset->acquired && !dataset->stale;
|
|
int wakeFd = -1;
|
|
if (delivered)
|
|
{
|
|
const uint64_t now = microtime();
|
|
dataset->deliveryDeadline = now >
|
|
UINT64_MAX - FILE_DELIVERY_GRACE_US ? UINT64_MAX :
|
|
now + FILE_DELIVERY_GRACE_US;
|
|
dataset->deliveryPending = true;
|
|
wakeFd = files.fuseStopFd;
|
|
}
|
|
LG_UNLOCK(files.lock);
|
|
if (wakeFd >= 0 && !wakeFuseThread(wakeFd))
|
|
DEBUG_WARN("Failed to schedule clipboard file delivery expiry: %s",
|
|
strerror(errno));
|
|
if (delivered)
|
|
reapRemoteDeliveries(true);
|
|
}
|
|
|
|
void clipboardFiles_remotePresentationRelease(uint64_t presentation)
|
|
{
|
|
if (!presentation)
|
|
return;
|
|
LG_LOCK(files.lock);
|
|
struct RemoteDataset * dataset = remoteDatasetByAcquisitionNL(presentation);
|
|
if (dataset && dataset->presentationPins)
|
|
--dataset->presentationPins;
|
|
const struct RemoteAction action = retireRemoteDatasetNL(dataset);
|
|
LG_UNLOCK(files.lock);
|
|
performRemoteAction(action);
|
|
}
|
|
|
|
bool clipboardFiles_remoteReady(uint64_t dataset)
|
|
{
|
|
LG_LOCK(files.lock);
|
|
struct RemoteDataset * found = remoteDatasetByIdNL(dataset);
|
|
const bool result = found && found->ready && found->current;
|
|
LG_UNLOCK(files.lock);
|
|
return result;
|
|
}
|
|
|
|
static struct RemoteRequest * takeInitialRequestNL(
|
|
struct RemoteDataset * dataset)
|
|
{
|
|
struct RemoteRequest * request = files.remoteRequests;
|
|
while (request && (request->dataset != dataset ||
|
|
request->reply != REMOTE_REPLY_INITIAL))
|
|
request = request->next;
|
|
return request ? remoteRequestTakePointerNL(request) : NULL;
|
|
}
|
|
|
|
static void cancelRetiredInitialRequest(struct RemoteRequest * request)
|
|
{
|
|
if (!request)
|
|
return;
|
|
const LG_ClipboardFileRequest descriptor = request->request;
|
|
if (request->transportStarted &&
|
|
!clipboard_fileCancel(descriptor.dataset, descriptor.request,
|
|
LG_CLIPBOARD_FILE_ERROR_CANCELLED))
|
|
DEBUG_WARN("Failed to cancel retired clipboard file request");
|
|
unregisterRemoteInterrupt(request);
|
|
completeRemoteRequest(request);
|
|
}
|
|
|
|
bool clipboardFiles_remoteOffer(uint64_t datasetId)
|
|
{
|
|
DEBUG_INFO("dataset=%lu",
|
|
datasetId);
|
|
if (!datasetId)
|
|
return false;
|
|
struct RemoteDataset * dataset = calloc(1, sizeof(*dataset));
|
|
struct RemoteNode * root = calloc(1, sizeof(*root));
|
|
if (!dataset || !root)
|
|
{
|
|
free(dataset);
|
|
free(root);
|
|
return false;
|
|
}
|
|
LG_LOCK(files.lock);
|
|
if (files.remoteCurrent && files.remoteCurrent->dataset == datasetId &&
|
|
!files.remoteCurrent->stale)
|
|
{
|
|
LG_UNLOCK(files.lock);
|
|
free(dataset);
|
|
free(root);
|
|
return true;
|
|
}
|
|
struct RemoteDataset * reusable = remoteDatasetByIdNL(datasetId);
|
|
if (reusable && reusable->stale)
|
|
reusable = NULL;
|
|
if (!reusable)
|
|
{
|
|
dataset->dataset = datasetId;
|
|
dataset->acquisition = nextAcquisitionNL();
|
|
dataset->ino = nextInodeNL();
|
|
dataset->current = true;
|
|
dataset->acquiring = true;
|
|
snprintf(dataset->name, sizeof(dataset->name),
|
|
"%016" PRIx64 "-%016" PRIx64,
|
|
datasetId, (uint64_t)dataset->ino);
|
|
*root = (struct RemoteNode)
|
|
{
|
|
.dataset = dataset,
|
|
.ino = dataset->ino,
|
|
.parent = FUSE_ROOT_ID,
|
|
.node = KVMFR_CLIPBOARD_FILE_ROOT_NODE,
|
|
.type = KVMFR_CLIPBOARD_FILE_TYPE_DIRECTORY,
|
|
.name = strdup(dataset->name),
|
|
};
|
|
if (!root->name)
|
|
{
|
|
LG_UNLOCK(files.lock);
|
|
free(dataset);
|
|
free(root);
|
|
return false;
|
|
}
|
|
}
|
|
struct RemoteDataset * previous = files.remoteCurrent;
|
|
struct RemoteRequest * cancelled = NULL;
|
|
struct RemoteAction previousAction = { 0 };
|
|
if (previous)
|
|
{
|
|
previous->current = false;
|
|
cancelled = takeInitialRequestNL(previous);
|
|
previousAction = retireRemoteDatasetNL(previous);
|
|
}
|
|
if (reusable)
|
|
{
|
|
reusable->current = true;
|
|
files.remoteCurrent = reusable;
|
|
LG_UNLOCK(files.lock);
|
|
cancelRetiredInitialRequest(cancelled);
|
|
performRemoteAction(previousAction);
|
|
reapRemoteDeliveries(true);
|
|
free(dataset);
|
|
free(root);
|
|
return true;
|
|
}
|
|
dataset->next = files.remoteDatasets;
|
|
files.remoteDatasets = dataset;
|
|
files.remoteCurrent = dataset;
|
|
root->next = files.remoteNodes;
|
|
files.remoteNodes = root;
|
|
const uint64_t acquisition = dataset->acquisition;
|
|
LG_UNLOCK(files.lock);
|
|
cancelRetiredInitialRequest(cancelled);
|
|
performRemoteAction(previousAction);
|
|
reapRemoteDeliveries(true);
|
|
if (!clipboard_fileAcquire(datasetId, acquisition))
|
|
{
|
|
clipboardFiles_remoteCancel(datasetId, acquisition,
|
|
LG_CLIPBOARD_FILE_ERROR_DISCONNECTED);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void clipboardFiles_remoteClear(void)
|
|
{
|
|
LG_LOCK(files.lock);
|
|
struct RemoteDataset * previous = files.remoteCurrent;
|
|
struct RemoteRequest * cancelled = NULL;
|
|
struct RemoteAction action = { 0 };
|
|
if (previous)
|
|
{
|
|
previous->current = false;
|
|
cancelled = takeInitialRequestNL(previous);
|
|
action = retireRemoteDatasetNL(previous);
|
|
}
|
|
files.remoteCurrent = NULL;
|
|
LG_UNLOCK(files.lock);
|
|
cancelRetiredInitialRequest(cancelled);
|
|
performRemoteAction(action);
|
|
}
|
|
|
|
void clipboardFiles_providerUnavailable(void)
|
|
{
|
|
if (!files.initialized)
|
|
return;
|
|
|
|
LG_LOCK(files.lock);
|
|
files.remoteCurrent = NULL;
|
|
for (struct RemoteDataset * dataset = files.remoteDatasets;
|
|
dataset; dataset = dataset->next)
|
|
{
|
|
dataset->current = false;
|
|
dataset->acquiring = false;
|
|
dataset->acquired = false;
|
|
dataset->stale = true;
|
|
dataset->deliveryPending = false;
|
|
dataset->deliveryDeadline = 0;
|
|
}
|
|
struct RemoteRequest * remoteRequests = files.remoteRequests;
|
|
files.remoteRequests = NULL;
|
|
|
|
struct LocalAcquisition * localAcquisitions = files.localAcquisitions;
|
|
files.localAcquisitions = NULL;
|
|
for (struct LocalAcquisition * acquisition = localAcquisitions;
|
|
acquisition; acquisition = acquisition->next)
|
|
if (acquisition->dataset->references)
|
|
--acquisition->dataset->references;
|
|
|
|
struct LocalDataset * retired = NULL;
|
|
struct LocalDataset ** datasetLink = &files.localDatasets;
|
|
while (*datasetLink)
|
|
{
|
|
struct LocalDataset * dataset = *datasetLink;
|
|
if (dataset->current || dataset->references)
|
|
{
|
|
datasetLink = &dataset->next;
|
|
continue;
|
|
}
|
|
*datasetLink = dataset->next;
|
|
dataset->next = retired;
|
|
retired = dataset;
|
|
}
|
|
|
|
for (struct LocalJob * job = files.jobsHead; job; job = job->next)
|
|
atomic_store_explicit(&job->cancelled, true, memory_order_release);
|
|
for (struct LocalJob * job = files.activeJobs; job; job = job->next)
|
|
{
|
|
atomic_store_explicit(&job->cancelled, true, memory_order_release);
|
|
lgSignalEvent(job->ready);
|
|
}
|
|
LG_UNLOCK(files.lock);
|
|
if (files.jobEvent)
|
|
lgSignalEvent(files.jobEvent);
|
|
|
|
while (remoteRequests)
|
|
{
|
|
struct RemoteRequest * next = remoteRequests->next;
|
|
unregisterRemoteInterrupt(remoteRequests);
|
|
if (remoteRequests->fuseRequest)
|
|
fuse_reply_err(remoteRequests->fuseRequest, EIO);
|
|
completeRemoteRequest(remoteRequests);
|
|
remoteRequests = next;
|
|
}
|
|
pruneUnusedRemoteDatasets();
|
|
while (localAcquisitions)
|
|
{
|
|
struct LocalAcquisition * next = localAcquisitions->next;
|
|
free(localAcquisitions);
|
|
localAcquisitions = next;
|
|
}
|
|
while (retired)
|
|
{
|
|
struct LocalDataset * next = retired->next;
|
|
freeLocalDataset(retired);
|
|
retired = next;
|
|
}
|
|
}
|
|
|
|
void clipboardFiles_remoteAcquired(uint64_t datasetId,
|
|
uint64_t acquisition, LG_ClipboardFileError error)
|
|
{
|
|
LG_LOCK(files.lock);
|
|
struct RemoteDataset * dataset = remoteDatasetByAcquisitionNL(acquisition);
|
|
const bool valid = dataset && dataset->dataset == datasetId &&
|
|
dataset->acquiring && !dataset->stale;
|
|
if (valid)
|
|
{
|
|
dataset->acquiring = false;
|
|
dataset->acquired = !error;
|
|
}
|
|
struct RemoteNode * root = valid ? datasetRootNL(dataset) : NULL;
|
|
const bool pinned = valid && !error && root &&
|
|
pinRemoteOperationNL(dataset, false);
|
|
const uint64_t failedDataset = valid && (error || !pinned) ?
|
|
failCurrentRemoteDatasetNL(dataset) : 0;
|
|
const struct RemoteAction action = valid ?
|
|
retireRemoteDatasetNL(dataset) : (struct RemoteAction) { 0 };
|
|
LG_UNLOCK(files.lock);
|
|
if (valid && error)
|
|
DEBUG_WARN("failed: dataset=%lu, acquisition=%lu, error=%u (%s)",
|
|
datasetId, acquisition, (unsigned)error, fileErrorName(error));
|
|
else if (valid)
|
|
DEBUG_INFO("dataset=%lu, acquisition=%lu", datasetId, acquisition);
|
|
performRemoteAction(action);
|
|
if (failedDataset)
|
|
clipboard_fileRemoteFailed(failedDataset);
|
|
if (!pinned)
|
|
return;
|
|
if (!requestRemote(dataset, root, LG_CLIPBOARD_FILE_LIST,
|
|
0, 0, REMOTE_REPLY_INITIAL, NULL, 0, 0, NULL))
|
|
{
|
|
LG_LOCK(files.lock);
|
|
const uint64_t unsentDataset =
|
|
failCurrentRemoteDatasetNL(dataset);
|
|
const struct RemoteAction failed = retireRemoteDatasetNL(dataset);
|
|
LG_UNLOCK(files.lock);
|
|
performRemoteAction(failed);
|
|
if (unsentDataset)
|
|
clipboard_fileRemoteFailed(unsentDataset);
|
|
}
|
|
releaseRemoteOperation(dataset);
|
|
}
|
|
|
|
static bool sameRemoteFileRequest(
|
|
const struct RemoteRequest * request,
|
|
const LG_ClipboardFileRequest * descriptor)
|
|
{
|
|
return request->request.dataset == descriptor->dataset &&
|
|
request->request.request == descriptor->request &&
|
|
request->request.node == descriptor->node &&
|
|
request->request.offset == descriptor->offset &&
|
|
request->request.length == descriptor->length &&
|
|
request->request.operation == descriptor->operation;
|
|
}
|
|
|
|
LG_ClipboardResult clipboardFiles_remoteDataBegin(
|
|
const LG_ClipboardFileRequest * descriptor, uint64_t sizeHint)
|
|
{
|
|
if (!descriptor)
|
|
return LG_CLIPBOARD_RESULT_FAILED;
|
|
LG_LOCK(files.lock);
|
|
struct RemoteRequest * request = files.remoteRequests;
|
|
while (request && request->request.request != descriptor->request)
|
|
request = request->next;
|
|
const bool valid = request && !request->began &&
|
|
sameRemoteFileRequest(request, descriptor) &&
|
|
(descriptor->operation != LG_CLIPBOARD_FILE_READ ||
|
|
sizeHint == descriptor->length);
|
|
if (valid)
|
|
{
|
|
request->began = true;
|
|
request->sizeHint = sizeHint;
|
|
}
|
|
struct RemoteRequest * failed = !valid && request ?
|
|
remoteRequestTakeNL(descriptor->request) : NULL;
|
|
LG_UNLOCK(files.lock);
|
|
failRemoteRequest(failed, EIO);
|
|
return valid ? LG_CLIPBOARD_RESULT_ACCEPTED :
|
|
LG_CLIPBOARD_RESULT_FAILED;
|
|
}
|
|
|
|
LG_ClipboardResult clipboardFiles_remoteDataChunk(
|
|
const LG_ClipboardFileRequest * descriptor, uint64_t responseOffset,
|
|
const void * data, size_t size)
|
|
{
|
|
if (!descriptor)
|
|
return LG_CLIPBOARD_RESULT_FAILED;
|
|
const bool envelopeValid = data && size && responseOffset <= SIZE_MAX &&
|
|
size <= SIZE_MAX - (size_t)responseOffset;
|
|
LG_LOCK(files.lock);
|
|
struct RemoteRequest * request = files.remoteRequests;
|
|
while (request && request->request.request != descriptor->request)
|
|
request = request->next;
|
|
const size_t end = envelopeValid ? (size_t)responseOffset + size : 0;
|
|
const bool valid = envelopeValid && request && request->began &&
|
|
sameRemoteFileRequest(request, descriptor) &&
|
|
responseOffset == request->dataSize &&
|
|
(request->sizeHint == KVMFR_CLIPBOARD_SIZE_UNKNOWN ||
|
|
end <= request->sizeHint) &&
|
|
(descriptor->operation != LG_CLIPBOARD_FILE_READ ||
|
|
end <= descriptor->length) &&
|
|
grow((void **)&request->data, &request->dataCapacity, end);
|
|
if (valid)
|
|
{
|
|
memcpy(request->data + request->dataSize, data, size);
|
|
request->dataSize = end;
|
|
}
|
|
struct RemoteRequest * failed = !valid && request ?
|
|
remoteRequestTakeNL(descriptor->request) : NULL;
|
|
LG_UNLOCK(files.lock);
|
|
failRemoteRequest(failed, EIO);
|
|
return valid ? LG_CLIPBOARD_RESULT_ACCEPTED :
|
|
LG_CLIPBOARD_RESULT_FAILED;
|
|
}
|
|
|
|
LG_ClipboardResult clipboardFiles_remoteDataEnd(
|
|
const LG_ClipboardFileRequest * descriptor, uint64_t finalSize)
|
|
{
|
|
if (!descriptor)
|
|
return LG_CLIPBOARD_RESULT_FAILED;
|
|
LG_LOCK(files.lock);
|
|
struct RemoteRequest * request = remoteRequestTakeNL(descriptor->request);
|
|
const bool valid = finalSize <= SIZE_MAX && request && request->began &&
|
|
sameRemoteFileRequest(request, descriptor) &&
|
|
request->dataSize == (size_t)finalSize &&
|
|
(request->sizeHint == KVMFR_CLIPBOARD_SIZE_UNKNOWN ||
|
|
request->sizeHint == finalSize) &&
|
|
(descriptor->operation != LG_CLIPBOARD_FILE_READ ||
|
|
finalSize == descriptor->length);
|
|
LG_UNLOCK(files.lock);
|
|
if (!valid)
|
|
{
|
|
failRemoteRequest(request, EIO);
|
|
return LG_CLIPBOARD_RESULT_FAILED;
|
|
}
|
|
finishRemoteRequest(request);
|
|
return LG_CLIPBOARD_RESULT_ACCEPTED;
|
|
}
|
|
|
|
void clipboardFiles_remoteCancel(uint64_t dataset,
|
|
uint64_t requestId, LG_ClipboardFileError reason)
|
|
{
|
|
LG_LOCK(files.lock);
|
|
struct RemoteRequest * request = remoteRequestTakeNL(requestId);
|
|
bool acquisition = false;
|
|
uint64_t failedDataset = 0;
|
|
struct RemoteAction action = { 0 };
|
|
for (struct RemoteDataset * item = files.remoteDatasets;
|
|
item; item = item->next)
|
|
if (item->dataset == dataset && item->acquisition == requestId)
|
|
{
|
|
item->acquiring = false;
|
|
item->acquired = false;
|
|
failedDataset = failCurrentRemoteDatasetNL(item);
|
|
item->ready = false;
|
|
item->current = false;
|
|
item->stale = true;
|
|
item->deliveryPending = false;
|
|
item->deliveryDeadline = 0;
|
|
if (files.remoteCurrent == item)
|
|
files.remoteCurrent = NULL;
|
|
acquisition = true;
|
|
action = retireRemoteDatasetNL(item);
|
|
break;
|
|
}
|
|
LG_UNLOCK(files.lock);
|
|
if (acquisition)
|
|
DEBUG_WARN("Remote clipboard file acquisition failed: "
|
|
"dataset=%lu, acquisition=%lu, error=%u (%s)",
|
|
dataset, requestId, (unsigned)reason, fileErrorName(reason));
|
|
if (acquisition && !request)
|
|
{
|
|
performRemoteAction(action);
|
|
if (failedDataset)
|
|
clipboard_fileRemoteFailed(failedDataset);
|
|
return;
|
|
}
|
|
if (!request || request->request.dataset != dataset)
|
|
{
|
|
failRemoteRequest(request, EIO);
|
|
performRemoteAction(action);
|
|
if (failedDataset)
|
|
clipboard_fileRemoteFailed(failedDataset);
|
|
return;
|
|
}
|
|
failRemoteRequest(request, errnoFromFileError(reason));
|
|
performRemoteAction(action);
|
|
if (failedDataset)
|
|
clipboard_fileRemoteFailed(failedDataset);
|
|
}
|
|
|
|
void clipboardFiles_localAcquire(uint64_t wireDataset,
|
|
uint64_t acquisition)
|
|
{
|
|
LG_ClipboardFileError error = LG_CLIPBOARD_FILE_ERROR_STALE;
|
|
struct LocalAcquisition * item = calloc(1, sizeof(*item));
|
|
LG_LOCK(files.lock);
|
|
struct LocalDataset * dataset = files.localDatasets;
|
|
while (dataset && dataset->dataset != wireDataset)
|
|
dataset = dataset->next;
|
|
if (dataset && item)
|
|
{
|
|
item->dataset = dataset;
|
|
item->wireDataset = wireDataset;
|
|
item->acquisition = acquisition;
|
|
item->next = files.localAcquisitions;
|
|
files.localAcquisitions = item;
|
|
++dataset->references;
|
|
error = LG_CLIPBOARD_FILE_ERROR_NONE;
|
|
}
|
|
else if (!item)
|
|
error = LG_CLIPBOARD_FILE_ERROR_NO_MEMORY;
|
|
LG_UNLOCK(files.lock);
|
|
if (error)
|
|
free(item);
|
|
clipboard_fileAcquired(wireDataset, acquisition, error);
|
|
}
|
|
|
|
void clipboardFiles_localRelease(uint64_t wireDataset,
|
|
uint64_t acquisition)
|
|
{
|
|
LG_LOCK(files.lock);
|
|
struct LocalAcquisition ** link = &files.localAcquisitions;
|
|
while (*link && ((*link)->wireDataset != wireDataset ||
|
|
(*link)->acquisition != acquisition))
|
|
link = &(*link)->next;
|
|
struct LocalAcquisition * item = *link;
|
|
struct LocalDataset * retired = NULL;
|
|
if (item)
|
|
{
|
|
*link = item->next;
|
|
if (item->dataset->references)
|
|
--item->dataset->references;
|
|
retired = takeUnusedLocalDatasetNL(item->dataset);
|
|
}
|
|
LG_UNLOCK(files.lock);
|
|
free(item);
|
|
freeLocalDataset(retired);
|
|
}
|
|
|
|
static struct LocalDataset * acquiredLocalDatasetNL(uint64_t wireDataset)
|
|
{
|
|
for (struct LocalDataset * dataset = files.localDatasets;
|
|
dataset; dataset = dataset->next)
|
|
if (dataset->dataset == wireDataset)
|
|
return dataset;
|
|
return NULL;
|
|
}
|
|
|
|
static bool sendJobCall(struct LocalJob * job, int operation,
|
|
uint64_t offset, const void * data, size_t size, bool end)
|
|
{
|
|
for (;;)
|
|
{
|
|
if (atomic_load_explicit(&job->cancelled, memory_order_acquire))
|
|
return false;
|
|
LG_ClipboardResult result;
|
|
switch (operation)
|
|
{
|
|
case 0:
|
|
result = clipboard_fileDataBegin(&job->request, offset);
|
|
break;
|
|
case 1:
|
|
result = clipboard_fileDataChunk(
|
|
&job->request, offset, data, size, end);
|
|
break;
|
|
default:
|
|
result = clipboard_fileDataEnd(&job->request, offset);
|
|
break;
|
|
}
|
|
if (result == LG_CLIPBOARD_RESULT_ACCEPTED)
|
|
return true;
|
|
if (result == LG_CLIPBOARD_RESULT_FAILED)
|
|
return false;
|
|
if (!lgWaitEvent(job->ready, TIMEOUT_INFINITE))
|
|
return false;
|
|
}
|
|
}
|
|
|
|
static bool sendBufferJob(struct LocalJob * job,
|
|
const uint8_t * data, size_t size)
|
|
{
|
|
if (!sendJobCall(job, 0, size, NULL, 0, false))
|
|
return false;
|
|
size_t offset = 0;
|
|
while (offset < size)
|
|
{
|
|
size_t chunk = size - offset;
|
|
if (chunk > FILE_STREAM_CHUNK)
|
|
chunk = FILE_STREAM_CHUNK;
|
|
const bool end = chunk == size - offset;
|
|
if (!sendJobCall(job, 1, offset, data + offset, chunk, end))
|
|
return false;
|
|
offset += chunk;
|
|
}
|
|
return sendJobCall(job, 2, size, NULL, 0, true);
|
|
}
|
|
|
|
static LG_ClipboardFileError appendLocalEntry(uint8_t ** data, size_t * size,
|
|
size_t * capacity, const struct LocalNode * node)
|
|
{
|
|
const size_t nameLength = strlen(node->name);
|
|
if (!nameLength || nameLength > NAME_MAX ||
|
|
!validUtf8((const uint8_t *)node->name, nameLength))
|
|
return LG_CLIPBOARD_FILE_ERROR_NOT_SUPPORTED;
|
|
const uint64_t bytes64 = KVMFR_CLIPBOARD_FILE_ENTRY_BYTES(nameLength);
|
|
if (bytes64 > SIZE_MAX || *size > SIZE_MAX - (size_t)bytes64)
|
|
return LG_CLIPBOARD_FILE_ERROR_NO_MEMORY;
|
|
const size_t wanted = *size + (size_t)bytes64;
|
|
if (!wanted || !grow((void **)data, capacity, wanted) || !*data)
|
|
return LG_CLIPBOARD_FILE_ERROR_NO_MEMORY;
|
|
KVMFRClipboardFileEntry entry =
|
|
{
|
|
.node = node->node,
|
|
.size = node->size,
|
|
.createdNs = node->createdNs,
|
|
.modifiedNs = node->modifiedNs,
|
|
.type = node->type,
|
|
.nameLength = (uint32_t)nameLength,
|
|
};
|
|
memcpy(*data + *size, &entry, sizeof(entry));
|
|
memcpy(*data + *size + sizeof(entry), node->name, nameLength);
|
|
memset(*data + *size + sizeof(entry) + nameLength, 0,
|
|
(size_t)bytes64 - sizeof(entry) - nameLength);
|
|
*size += (size_t)bytes64;
|
|
return LG_CLIPBOARD_FILE_ERROR_NONE;
|
|
}
|
|
|
|
static bool localJobCancelled(const struct LocalJob * job)
|
|
{
|
|
return atomic_load_explicit(&job->cancelled, memory_order_acquire);
|
|
}
|
|
|
|
static LG_ClipboardFileError localList(struct LocalJob * job)
|
|
{
|
|
uint8_t * data = NULL;
|
|
size_t size = 0;
|
|
size_t capacity = 0;
|
|
struct LocalNode ** listed = NULL;
|
|
size_t listedCount = 0;
|
|
size_t listedCapacity = 0;
|
|
LG_ClipboardFileError error = LG_CLIPBOARD_FILE_ERROR_IO;
|
|
LG_LOCK(files.lock);
|
|
struct LocalNode * directory = job->request.node ?
|
|
localNodeByIdNL(job->dataset, job->request.node) : NULL;
|
|
if (job->request.node && !directory)
|
|
{
|
|
LG_UNLOCK(files.lock);
|
|
return LG_CLIPBOARD_FILE_ERROR_NOT_FOUND;
|
|
}
|
|
if (directory &&
|
|
directory->type != KVMFR_CLIPBOARD_FILE_TYPE_DIRECTORY)
|
|
{
|
|
LG_UNLOCK(files.lock);
|
|
return LG_CLIPBOARD_FILE_ERROR_NOT_DIRECTORY;
|
|
}
|
|
if (!job->request.node)
|
|
{
|
|
struct LocalNode ** roots = NULL;
|
|
size_t rootCount = 0;
|
|
size_t rootCapacity = 0;
|
|
for (struct LocalNode * node = job->dataset->nodes;
|
|
node; node = node->next)
|
|
if (!node->parent)
|
|
{
|
|
if (rootCount == SIZE_MAX / sizeof(*roots))
|
|
{
|
|
LG_UNLOCK(files.lock);
|
|
free(roots);
|
|
return LG_CLIPBOARD_FILE_ERROR_NO_MEMORY;
|
|
}
|
|
if (!grow((void **)&roots, &rootCapacity,
|
|
(rootCount + 1U) * sizeof(*roots)))
|
|
{
|
|
LG_UNLOCK(files.lock);
|
|
free(roots);
|
|
return LG_CLIPBOARD_FILE_ERROR_NO_MEMORY;
|
|
}
|
|
roots[rootCount++] = node;
|
|
}
|
|
LG_UNLOCK(files.lock);
|
|
|
|
for (size_t i = 0; i < rootCount; ++i)
|
|
{
|
|
if (localJobCancelled(job))
|
|
{
|
|
free(roots);
|
|
free(data);
|
|
return LG_CLIPBOARD_FILE_ERROR_CANCELLED;
|
|
}
|
|
struct stat value;
|
|
const int fd = openLocalNode(roots[i], &value);
|
|
if (fd < 0)
|
|
{
|
|
error = fileErrorFromErrno(errno);
|
|
free(roots);
|
|
free(data);
|
|
return error;
|
|
}
|
|
close(fd);
|
|
error = appendLocalEntry(&data, &size, &capacity, roots[i]);
|
|
if (error)
|
|
{
|
|
free(roots);
|
|
free(data);
|
|
return error;
|
|
}
|
|
}
|
|
for (size_t i = 0; i < rootCount; ++i)
|
|
{
|
|
if (localJobCancelled(job))
|
|
{
|
|
free(roots);
|
|
free(data);
|
|
return LG_CLIPBOARD_FILE_ERROR_CANCELLED;
|
|
}
|
|
struct stat value;
|
|
const int fd = openLocalNode(roots[i], &value);
|
|
if (fd < 0)
|
|
{
|
|
error = fileErrorFromErrno(errno);
|
|
free(roots);
|
|
free(data);
|
|
return error;
|
|
}
|
|
close(fd);
|
|
}
|
|
free(roots);
|
|
const bool result = sendBufferJob(job, data, size);
|
|
free(data);
|
|
return result ? LG_CLIPBOARD_FILE_ERROR_NONE :
|
|
localJobCancelled(job) ? LG_CLIPBOARD_FILE_ERROR_CANCELLED :
|
|
LG_CLIPBOARD_FILE_ERROR_DISCONNECTED;
|
|
}
|
|
LG_UNLOCK(files.lock);
|
|
|
|
if (localJobCancelled(job))
|
|
return LG_CLIPBOARD_FILE_ERROR_CANCELLED;
|
|
struct stat directoryBefore;
|
|
const int directoryFd = openLocalNode(directory, &directoryBefore);
|
|
if (directoryFd < 0)
|
|
return fileErrorFromErrno(errno);
|
|
DIR * dir = fdopendir(directoryFd);
|
|
if (!dir)
|
|
{
|
|
error = fileErrorFromErrno(errno);
|
|
close(directoryFd);
|
|
return error;
|
|
}
|
|
|
|
for (;;)
|
|
{
|
|
if (localJobCancelled(job))
|
|
{
|
|
error = LG_CLIPBOARD_FILE_ERROR_CANCELLED;
|
|
goto failed;
|
|
}
|
|
errno = 0;
|
|
struct dirent * entry = readdir(dir);
|
|
if (!entry)
|
|
{
|
|
if (errno)
|
|
{
|
|
error = fileErrorFromErrno(errno);
|
|
goto failed;
|
|
}
|
|
break;
|
|
}
|
|
if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, ".."))
|
|
continue;
|
|
|
|
struct stat before;
|
|
if (fstatat(directoryFd, entry->d_name, &before,
|
|
AT_SYMLINK_NOFOLLOW) < 0)
|
|
{
|
|
error = fileErrorFromErrno(errno);
|
|
goto failed;
|
|
}
|
|
if (S_ISLNK(before.st_mode))
|
|
{
|
|
error = LG_CLIPBOARD_FILE_ERROR_NOT_SUPPORTED;
|
|
goto failed;
|
|
}
|
|
if (!localStatSupported(&before))
|
|
{
|
|
error = LG_CLIPBOARD_FILE_ERROR_NOT_SUPPORTED;
|
|
goto failed;
|
|
}
|
|
|
|
int flags = O_RDONLY | O_CLOEXEC | O_NOFOLLOW | O_NONBLOCK;
|
|
if (S_ISDIR(before.st_mode))
|
|
flags |= O_DIRECTORY;
|
|
const int childFd = openat(directoryFd, entry->d_name, flags);
|
|
if (childFd < 0)
|
|
{
|
|
error = fileErrorFromErrno(errno);
|
|
goto failed;
|
|
}
|
|
struct stat value;
|
|
if (fstat(childFd, &value) < 0)
|
|
{
|
|
error = fileErrorFromErrno(errno);
|
|
close(childFd);
|
|
goto failed;
|
|
}
|
|
const bool stable = localStatEqual(&before, &value);
|
|
close(childFd);
|
|
if (!stable)
|
|
{
|
|
error = LG_CLIPBOARD_FILE_ERROR_STALE;
|
|
goto failed;
|
|
}
|
|
|
|
const size_t parentLength = strlen(directory->path);
|
|
const size_t nameLength = strlen(entry->d_name);
|
|
if (parentLength > SIZE_MAX - nameLength - 2U)
|
|
{
|
|
error = LG_CLIPBOARD_FILE_ERROR_NO_MEMORY;
|
|
goto failed;
|
|
}
|
|
const size_t pathLength = parentLength + (parentLength ? 1U : 0U) +
|
|
nameLength + 1U;
|
|
char * childPath = malloc(pathLength);
|
|
if (!childPath)
|
|
{
|
|
error = LG_CLIPBOARD_FILE_ERROR_NO_MEMORY;
|
|
goto failed;
|
|
}
|
|
if (parentLength)
|
|
snprintf(childPath, pathLength, "%s/%s",
|
|
directory->path, entry->d_name);
|
|
else
|
|
memcpy(childPath, entry->d_name, nameLength + 1U);
|
|
|
|
LG_LOCK(files.lock);
|
|
struct LocalNode * node = localChildNL(
|
|
job->dataset, directory->node, entry->d_name);
|
|
if (!node)
|
|
node = addLocalNode(job->dataset, directory->node,
|
|
directory->root, childPath, entry->d_name, &value);
|
|
else
|
|
free(childPath);
|
|
if (!node)
|
|
error = LG_CLIPBOARD_FILE_ERROR_NO_MEMORY;
|
|
else if (!localNodeMatches(node, &value))
|
|
error = LG_CLIPBOARD_FILE_ERROR_STALE;
|
|
else
|
|
error = appendLocalEntry(&data, &size, &capacity, node);
|
|
LG_UNLOCK(files.lock);
|
|
if (error)
|
|
goto failed;
|
|
if (listedCount == SIZE_MAX / sizeof(*listed) ||
|
|
!grow((void **)&listed, &listedCapacity,
|
|
(listedCount + 1U) * sizeof(*listed)))
|
|
{
|
|
error = LG_CLIPBOARD_FILE_ERROR_NO_MEMORY;
|
|
goto failed;
|
|
}
|
|
listed[listedCount++] = node;
|
|
}
|
|
|
|
for (size_t i = 0; i < listedCount; ++i)
|
|
{
|
|
if (localJobCancelled(job))
|
|
{
|
|
error = LG_CLIPBOARD_FILE_ERROR_CANCELLED;
|
|
goto failed;
|
|
}
|
|
struct stat value;
|
|
const int fd = openLocalNode(listed[i], &value);
|
|
if (fd < 0)
|
|
{
|
|
error = fileErrorFromErrno(errno);
|
|
goto failed;
|
|
}
|
|
close(fd);
|
|
}
|
|
struct stat directoryAfter;
|
|
if (localJobCancelled(job))
|
|
{
|
|
error = LG_CLIPBOARD_FILE_ERROR_CANCELLED;
|
|
goto failed;
|
|
}
|
|
if (fstat(directoryFd, &directoryAfter) < 0)
|
|
{
|
|
error = fileErrorFromErrno(errno);
|
|
goto failed;
|
|
}
|
|
if (!localNodeMatches(directory, &directoryAfter))
|
|
{
|
|
error = LG_CLIPBOARD_FILE_ERROR_STALE;
|
|
goto failed;
|
|
}
|
|
if (closedir(dir) < 0)
|
|
{
|
|
const int closeError = errno;
|
|
free(listed);
|
|
free(data);
|
|
return fileErrorFromErrno(closeError);
|
|
}
|
|
free(listed);
|
|
const bool result = sendBufferJob(job, data, size);
|
|
free(data);
|
|
return result ? LG_CLIPBOARD_FILE_ERROR_NONE :
|
|
localJobCancelled(job) ? LG_CLIPBOARD_FILE_ERROR_CANCELLED :
|
|
LG_CLIPBOARD_FILE_ERROR_DISCONNECTED;
|
|
|
|
failed:
|
|
closedir(dir);
|
|
free(listed);
|
|
free(data);
|
|
return error;
|
|
}
|
|
|
|
static LG_ClipboardFileError localRead(struct LocalJob * job)
|
|
{
|
|
LG_LOCK(files.lock);
|
|
struct LocalNode * node = localNodeByIdNL(
|
|
job->dataset, job->request.node);
|
|
LG_UNLOCK(files.lock);
|
|
if (!node)
|
|
return LG_CLIPBOARD_FILE_ERROR_NOT_FOUND;
|
|
if (node->type != KVMFR_CLIPBOARD_FILE_TYPE_REGULAR)
|
|
return LG_CLIPBOARD_FILE_ERROR_IS_DIRECTORY;
|
|
if (job->request.offset > node->size ||
|
|
job->request.length > node->size - job->request.offset)
|
|
return LG_CLIPBOARD_FILE_ERROR_INVALID;
|
|
if (localJobCancelled(job))
|
|
return LG_CLIPBOARD_FILE_ERROR_CANCELLED;
|
|
|
|
struct stat before;
|
|
const int fd = openLocalNode(node, &before);
|
|
if (fd < 0)
|
|
return fileErrorFromErrno(errno);
|
|
uint8_t * buffer = malloc(job->request.length ? job->request.length : 1U);
|
|
if (!buffer)
|
|
{
|
|
close(fd);
|
|
return LG_CLIPBOARD_FILE_ERROR_NO_MEMORY;
|
|
}
|
|
LG_ClipboardFileError error = LG_CLIPBOARD_FILE_ERROR_NONE;
|
|
size_t received = 0;
|
|
while (received < job->request.length)
|
|
{
|
|
if (localJobCancelled(job))
|
|
{
|
|
error = LG_CLIPBOARD_FILE_ERROR_CANCELLED;
|
|
goto failed;
|
|
}
|
|
#ifdef ENABLE_TESTS
|
|
const bool forceEof = atomic_exchange_explicit(
|
|
&forceLocalEof, false, memory_order_acq_rel);
|
|
const ssize_t bytes = forceEof ? 0 :
|
|
pread(fd, buffer + received, job->request.length - received,
|
|
(off_t)(job->request.offset + received));
|
|
#else
|
|
const ssize_t bytes = pread(fd, buffer + received,
|
|
job->request.length - received,
|
|
(off_t)(job->request.offset + received));
|
|
#endif
|
|
if (bytes < 0 && errno == EINTR)
|
|
continue;
|
|
if (bytes < 0)
|
|
{
|
|
error = fileErrorFromErrno(errno);
|
|
goto failed;
|
|
}
|
|
if (!bytes)
|
|
{
|
|
error = LG_CLIPBOARD_FILE_ERROR_IO;
|
|
goto failed;
|
|
}
|
|
received += (size_t)bytes;
|
|
}
|
|
|
|
struct stat after;
|
|
if (localJobCancelled(job))
|
|
{
|
|
error = LG_CLIPBOARD_FILE_ERROR_CANCELLED;
|
|
goto failed;
|
|
}
|
|
if (fstat(fd, &after) < 0)
|
|
{
|
|
error = fileErrorFromErrno(errno);
|
|
goto failed;
|
|
}
|
|
if (!localNodeMatches(node, &after) ||
|
|
!localStatEqual(&before, &after))
|
|
{
|
|
error = LG_CLIPBOARD_FILE_ERROR_STALE;
|
|
goto failed;
|
|
}
|
|
if (received != job->request.length)
|
|
{
|
|
error = LG_CLIPBOARD_FILE_ERROR_IO;
|
|
goto failed;
|
|
}
|
|
close(fd);
|
|
const bool result = sendBufferJob(job, buffer, received);
|
|
free(buffer);
|
|
return result ? LG_CLIPBOARD_FILE_ERROR_NONE :
|
|
localJobCancelled(job) ? LG_CLIPBOARD_FILE_ERROR_CANCELLED :
|
|
LG_CLIPBOARD_FILE_ERROR_DISCONNECTED;
|
|
|
|
failed:
|
|
close(fd);
|
|
free(buffer);
|
|
return error;
|
|
}
|
|
|
|
static int localWorker(void * opaque)
|
|
{
|
|
(void)opaque;
|
|
for (;;)
|
|
{
|
|
LG_LOCK(files.lock);
|
|
struct LocalJob * job = files.jobsHead;
|
|
if (job)
|
|
{
|
|
files.jobsHead = job->next;
|
|
if (!files.jobsHead)
|
|
files.jobsTail = &files.jobsHead;
|
|
job->next = files.activeJobs;
|
|
files.activeJobs = job;
|
|
}
|
|
const bool stopping = files.stopping;
|
|
if (!job && !stopping)
|
|
lgResetEvent(files.jobEvent);
|
|
LG_UNLOCK(files.lock);
|
|
if (!job)
|
|
{
|
|
if (stopping)
|
|
return 0;
|
|
lgWaitEvent(files.jobEvent, TIMEOUT_INFINITE);
|
|
continue;
|
|
}
|
|
LG_ClipboardFileError error = LG_CLIPBOARD_FILE_ERROR_CANCELLED;
|
|
if (!atomic_load_explicit(&job->cancelled, memory_order_acquire))
|
|
error = job->request.operation == LG_CLIPBOARD_FILE_LIST ?
|
|
localList(job) : localRead(job);
|
|
if (error && error != LG_CLIPBOARD_FILE_ERROR_CANCELLED &&
|
|
!atomic_load_explicit(&job->cancelled, memory_order_acquire))
|
|
clipboard_fileCancel(job->request.dataset,
|
|
job->request.request, error);
|
|
LG_LOCK(files.lock);
|
|
struct LocalJob ** link = &files.activeJobs;
|
|
while (*link && *link != job)
|
|
link = &(*link)->next;
|
|
if (*link)
|
|
*link = job->next;
|
|
if (job->dataset->references)
|
|
--job->dataset->references;
|
|
struct LocalDataset * retired =
|
|
takeUnusedLocalDatasetNL(job->dataset);
|
|
LG_UNLOCK(files.lock);
|
|
freeLocalDataset(retired);
|
|
lgFreeEvent(job->ready);
|
|
free(job);
|
|
}
|
|
}
|
|
|
|
void clipboardFiles_localRequest(
|
|
const LG_ClipboardFileRequest * request)
|
|
{
|
|
if (!request || !request->request ||
|
|
(request->operation != LG_CLIPBOARD_FILE_LIST &&
|
|
request->operation != LG_CLIPBOARD_FILE_READ) ||
|
|
(request->operation == LG_CLIPBOARD_FILE_LIST &&
|
|
(request->offset || request->length)) ||
|
|
(request->operation == LG_CLIPBOARD_FILE_READ &&
|
|
(!request->length || request->length > FILE_REQUEST_MAX ||
|
|
request->offset > INT64_MAX ||
|
|
request->length > (uint64_t)INT64_MAX - request->offset)))
|
|
{
|
|
if (request)
|
|
clipboard_fileCancel(request->dataset, request->request,
|
|
LG_CLIPBOARD_FILE_ERROR_INVALID);
|
|
return;
|
|
}
|
|
struct LocalJob * job = calloc(1, sizeof(*job));
|
|
if (!job)
|
|
{
|
|
clipboard_fileCancel(request->dataset, request->request,
|
|
LG_CLIPBOARD_FILE_ERROR_NO_MEMORY);
|
|
return;
|
|
}
|
|
job->ready = lgCreateEvent(true, 0);
|
|
if (!job->ready)
|
|
{
|
|
free(job);
|
|
clipboard_fileCancel(request->dataset, request->request,
|
|
LG_CLIPBOARD_FILE_ERROR_NO_MEMORY);
|
|
return;
|
|
}
|
|
atomic_init(&job->cancelled, false);
|
|
job->request = *request;
|
|
LG_LOCK(files.lock);
|
|
job->dataset = acquiredLocalDatasetNL(request->dataset);
|
|
if (job->dataset && !files.stopping)
|
|
{
|
|
++job->dataset->references;
|
|
*files.jobsTail = job;
|
|
files.jobsTail = &job->next;
|
|
}
|
|
const bool queued = job->dataset && !files.stopping;
|
|
LG_UNLOCK(files.lock);
|
|
if (queued)
|
|
{
|
|
lgSignalEvent(files.jobEvent);
|
|
return;
|
|
}
|
|
lgFreeEvent(job->ready);
|
|
free(job);
|
|
clipboard_fileCancel(request->dataset, request->request,
|
|
LG_CLIPBOARD_FILE_ERROR_STALE);
|
|
}
|
|
|
|
void clipboardFiles_localCancel(uint64_t dataset,
|
|
uint64_t request, LG_ClipboardFileError reason)
|
|
{
|
|
(void)reason;
|
|
LG_LOCK(files.lock);
|
|
struct LocalAcquisition ** acquisition = &files.localAcquisitions;
|
|
struct LocalDataset * retired = NULL;
|
|
while (*acquisition && ((*acquisition)->wireDataset != dataset ||
|
|
(*acquisition)->acquisition != request))
|
|
acquisition = &(*acquisition)->next;
|
|
if (*acquisition)
|
|
{
|
|
struct LocalAcquisition * cancelled = *acquisition;
|
|
*acquisition = cancelled->next;
|
|
if (cancelled->dataset->references)
|
|
--cancelled->dataset->references;
|
|
retired = takeUnusedLocalDatasetNL(cancelled->dataset);
|
|
free(cancelled);
|
|
}
|
|
for (struct LocalJob * job = files.activeJobs; job; job = job->next)
|
|
if (job->request.dataset == dataset && job->request.request == request)
|
|
{
|
|
atomic_store_explicit(&job->cancelled, true, memory_order_release);
|
|
lgSignalEvent(job->ready);
|
|
}
|
|
for (struct LocalJob * job = files.jobsHead; job; job = job->next)
|
|
if (job->request.dataset == dataset && job->request.request == request)
|
|
atomic_store_explicit(&job->cancelled, true, memory_order_release);
|
|
LG_UNLOCK(files.lock);
|
|
freeLocalDataset(retired);
|
|
}
|
|
|
|
void clipboardFiles_localReady(uint64_t request)
|
|
{
|
|
LG_LOCK(files.lock);
|
|
for (struct LocalJob * job = files.activeJobs; job; job = job->next)
|
|
if (job->request.request == request)
|
|
lgSignalEvent(job->ready);
|
|
LG_UNLOCK(files.lock);
|
|
}
|
|
|
|
static void freeState(void)
|
|
{
|
|
struct RemoteRequest * request = files.remoteRequests;
|
|
while (request)
|
|
{
|
|
struct RemoteRequest * next = request->next;
|
|
unregisterRemoteInterrupt(request);
|
|
if (request->fuseRequest)
|
|
fuse_reply_err(request->fuseRequest, EIO);
|
|
freeRemoteRequest(request);
|
|
request = next;
|
|
}
|
|
struct RemoteNode * remoteNode = files.remoteNodes;
|
|
while (remoteNode)
|
|
{
|
|
struct RemoteNode * next = remoteNode->next;
|
|
free(remoteNode->name);
|
|
free(remoteNode);
|
|
remoteNode = next;
|
|
}
|
|
struct RemoteDataset * remote = files.remoteDatasets;
|
|
while (remote)
|
|
{
|
|
struct RemoteDataset * next = remote->next;
|
|
if (remote->acquired)
|
|
clipboard_fileRelease(remote->dataset, remote->acquisition);
|
|
else if (remote->acquiring)
|
|
clipboard_fileCancel(remote->dataset, remote->acquisition,
|
|
LG_CLIPBOARD_FILE_ERROR_CANCELLED);
|
|
free(remote);
|
|
remote = next;
|
|
}
|
|
struct LocalAcquisition * acquisition = files.localAcquisitions;
|
|
while (acquisition)
|
|
{
|
|
struct LocalAcquisition * next = acquisition->next;
|
|
free(acquisition);
|
|
acquisition = next;
|
|
}
|
|
struct LocalDataset * local = files.localDatasets;
|
|
while (local)
|
|
{
|
|
struct LocalDataset * next = local->next;
|
|
freeLocalDataset(local);
|
|
local = next;
|
|
}
|
|
}
|
|
|
|
bool clipboardFiles_init(void)
|
|
{
|
|
if (filesLockInitialized)
|
|
return false;
|
|
memset(&files, 0, sizeof(files));
|
|
files.fuseStopFd = -1;
|
|
LG_LOCK_INIT(files.lock);
|
|
filesLockInitialized = true;
|
|
uint64_t nonce = 0;
|
|
if (!randomBytes(&nonce, sizeof(nonce)))
|
|
{
|
|
DEBUG_ERROR("Failed to seed clipboard file transfer identifiers: %s",
|
|
strerror(errno));
|
|
goto failed;
|
|
}
|
|
nonce &= ~KVMFR_CLIPBOARD_TRANSFER_HELPER;
|
|
if (!nonce)
|
|
nonce = 1;
|
|
files.requestSerial = nonce;
|
|
files.datasetSerial = (nonce ^ UINT64_C(0x4c474346494c4553)) &
|
|
~KVMFR_CLIPBOARD_TRANSFER_HELPER;
|
|
if (!files.datasetSerial)
|
|
files.datasetSerial = 1;
|
|
files.inodeSerial = FUSE_ROOT_ID;
|
|
files.jobsTail = &files.jobsHead;
|
|
files.jobEvent = lgCreateEvent(false, 0);
|
|
if (!files.jobEvent)
|
|
goto failed;
|
|
files.fuseStopFd = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK);
|
|
if (files.fuseStopFd < 0)
|
|
{
|
|
DEBUG_ERROR("Failed to create the clipboard FUSE stop event: %s",
|
|
strerror(errno));
|
|
goto failed;
|
|
}
|
|
const char * runtime = getenv("XDG_RUNTIME_DIR");
|
|
if (!runtime || !*runtime)
|
|
{
|
|
DEBUG_ERROR("XDG_RUNTIME_DIR is required for clipboard file transfer");
|
|
goto failed;
|
|
}
|
|
const int length = snprintf(files.mountpoint, sizeof(files.mountpoint),
|
|
"%s/looking-glass-clipboard-XXXXXX", runtime);
|
|
if (length < 0 || (size_t)length >= sizeof(files.mountpoint) ||
|
|
!mkdtemp(files.mountpoint))
|
|
{
|
|
DEBUG_ERROR("Failed to create the clipboard FUSE mountpoint: %s",
|
|
strerror(errno));
|
|
goto failed;
|
|
}
|
|
chmod(files.mountpoint, 0700);
|
|
char mountOptions[160];
|
|
const int optionsLength = snprintf(mountOptions, sizeof(mountOptions),
|
|
"ro,nodev,nosuid,noexec,default_permissions,auto_unmount,max_read=%u",
|
|
(unsigned)FILE_REQUEST_MAX);
|
|
if (optionsLength < 0 || (size_t)optionsLength >= sizeof(mountOptions))
|
|
{
|
|
DEBUG_ERROR("Failed to format clipboard FUSE mount options");
|
|
goto failed;
|
|
}
|
|
struct fuse_args args = FUSE_ARGS_INIT(0, NULL);
|
|
if (fuse_opt_add_arg(&args, "looking-glass-client") < 0 ||
|
|
fuse_opt_add_arg(&args, "-o") < 0 ||
|
|
fuse_opt_add_arg(&args, mountOptions) < 0)
|
|
{
|
|
fuse_opt_free_args(&args);
|
|
goto failed;
|
|
}
|
|
files.session = fuse_session_new(&args, &fuseOps,
|
|
sizeof(fuseOps), NULL);
|
|
fuse_opt_free_args(&args);
|
|
if (!files.session || fuse_session_mount(
|
|
files.session, files.mountpoint) < 0)
|
|
{
|
|
DEBUG_ERROR("Failed to mount the clipboard FUSE filesystem");
|
|
goto failed;
|
|
}
|
|
if (!lgCreateThread("clipboard-fuse", fuseThread,
|
|
files.session, &files.fuseThread))
|
|
goto failed;
|
|
unsigned created = 0;
|
|
for (; created < FILE_WORKERS; ++created)
|
|
if (!lgCreateThread("clipboard-file", localWorker,
|
|
NULL, &files.workers[created]))
|
|
break;
|
|
if (created != FILE_WORKERS)
|
|
goto failed;
|
|
files.initialized = true;
|
|
DEBUG_INFO("Clipboard files mounted at %s", files.mountpoint);
|
|
return true;
|
|
|
|
failed:
|
|
clipboardFiles_free();
|
|
return false;
|
|
}
|
|
|
|
#ifdef ENABLE_TESTS
|
|
bool clipboardFiles_testFuseStopWake(void)
|
|
{
|
|
const int stopFd = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK);
|
|
if (stopFd < 0)
|
|
return false;
|
|
const bool result = wakeFuseThread(stopFd) &&
|
|
waitFuseEvent(-1, stopFd, -1) == FUSE_WAIT_WAKE;
|
|
close(stopFd);
|
|
return result;
|
|
}
|
|
|
|
bool clipboardFiles_testUnsentRemoteOwnership(void)
|
|
{
|
|
return finishUnsentRemoteRequest(0);
|
|
}
|
|
|
|
bool clipboardFiles_testInit(uint64_t nonce)
|
|
{
|
|
if (filesLockInitialized)
|
|
return false;
|
|
nonce &= ~KVMFR_CLIPBOARD_TRANSFER_HELPER;
|
|
if (!nonce)
|
|
return false;
|
|
memset(&files, 0, sizeof(files));
|
|
files.fuseStopFd = -1;
|
|
LG_LOCK_INIT(files.lock);
|
|
filesLockInitialized = true;
|
|
files.requestSerial = nonce;
|
|
files.datasetSerial = (nonce ^ UINT64_C(0x4c474346494c4553)) &
|
|
~KVMFR_CLIPBOARD_TRANSFER_HELPER;
|
|
if (!files.datasetSerial)
|
|
files.datasetSerial = 1;
|
|
files.inodeSerial = FUSE_ROOT_ID;
|
|
files.jobsTail = &files.jobsHead;
|
|
files.initialized = true;
|
|
atomic_store_explicit(&forceLocalEof, false, memory_order_release);
|
|
return true;
|
|
}
|
|
|
|
size_t clipboardFiles_testRemoteDatasetCount(void)
|
|
{
|
|
if (!filesLockInitialized)
|
|
return 0;
|
|
size_t count = 0;
|
|
LG_LOCK(files.lock);
|
|
for (struct RemoteDataset * dataset = files.remoteDatasets;
|
|
dataset; dataset = dataset->next)
|
|
++count;
|
|
LG_UNLOCK(files.lock);
|
|
return count;
|
|
}
|
|
|
|
void clipboardFiles_testExpireRemoteDeliveries(void)
|
|
{
|
|
if (!filesLockInitialized)
|
|
return;
|
|
LG_LOCK(files.lock);
|
|
for (struct RemoteDataset * dataset = files.remoteDatasets;
|
|
dataset; dataset = dataset->next)
|
|
if (dataset->deliveryPending)
|
|
dataset->deliveryDeadline = 0;
|
|
LG_UNLOCK(files.lock);
|
|
reapRemoteDeliveries(false);
|
|
}
|
|
|
|
bool clipboardFiles_testBeginRemoteLookup(uint64_t presentation)
|
|
{
|
|
LG_LOCK(files.lock);
|
|
struct RemoteDataset * dataset =
|
|
remoteDatasetByAcquisitionNL(presentation);
|
|
const bool result = dataset && dataset->ready &&
|
|
pinRemoteOperationNL(dataset, true);
|
|
LG_UNLOCK(files.lock);
|
|
return result;
|
|
}
|
|
|
|
void clipboardFiles_testEndRemoteLookup(uint64_t presentation)
|
|
{
|
|
LG_LOCK(files.lock);
|
|
struct RemoteDataset * dataset =
|
|
remoteDatasetByAcquisitionNL(presentation);
|
|
if (dataset && dataset->operationPins)
|
|
--dataset->operationPins;
|
|
const struct RemoteAction action = retireRemoteDatasetNL(dataset);
|
|
LG_UNLOCK(files.lock);
|
|
performRemoteAction(action);
|
|
}
|
|
|
|
bool clipboardFiles_testRemoteRead(uint64_t presentation,
|
|
uint64_t nodeId, uint64_t offset, uint32_t length)
|
|
{
|
|
LG_LOCK(files.lock);
|
|
struct RemoteDataset * dataset =
|
|
remoteDatasetByAcquisitionNL(presentation);
|
|
struct RemoteNode * node = dataset ?
|
|
remoteNodeByWireNL(dataset, nodeId) : NULL;
|
|
const bool pinned = node && length &&
|
|
pinRemoteOperationNL(dataset, false);
|
|
LG_UNLOCK(files.lock);
|
|
if (!pinned)
|
|
return false;
|
|
const bool result = requestRemote(dataset, node,
|
|
LG_CLIPBOARD_FILE_READ, offset, length, REMOTE_REPLY_READ,
|
|
NULL, 0, 0, NULL);
|
|
releaseRemoteOperation(dataset);
|
|
return result;
|
|
}
|
|
|
|
void clipboardFiles_testForceLocalEof(void)
|
|
{
|
|
atomic_store_explicit(&forceLocalEof, true, memory_order_release);
|
|
}
|
|
|
|
bool clipboardFiles_testLocalRequest(
|
|
const LG_ClipboardFileRequest * request)
|
|
{
|
|
if (!request ||
|
|
(request->operation != LG_CLIPBOARD_FILE_LIST &&
|
|
request->operation != LG_CLIPBOARD_FILE_READ))
|
|
return false;
|
|
|
|
struct LocalJob job = { .request = *request };
|
|
atomic_init(&job.cancelled, false);
|
|
LG_LOCK(files.lock);
|
|
job.dataset = acquiredLocalDatasetNL(request->dataset);
|
|
if (job.dataset)
|
|
++job.dataset->references;
|
|
LG_UNLOCK(files.lock);
|
|
if (!job.dataset)
|
|
return false;
|
|
|
|
const LG_ClipboardFileError error =
|
|
request->operation == LG_CLIPBOARD_FILE_LIST ?
|
|
localList(&job) : localRead(&job);
|
|
if (error && error != LG_CLIPBOARD_FILE_ERROR_CANCELLED)
|
|
clipboard_fileCancel(request->dataset, request->request, error);
|
|
LG_LOCK(files.lock);
|
|
if (job.dataset->references)
|
|
--job.dataset->references;
|
|
LG_UNLOCK(files.lock);
|
|
return error == LG_CLIPBOARD_FILE_ERROR_NONE;
|
|
}
|
|
#endif
|
|
|
|
void clipboardFiles_free(void)
|
|
{
|
|
if (!filesLockInitialized)
|
|
return;
|
|
LG_LOCK(files.lock);
|
|
files.stopping = true;
|
|
for (struct LocalJob * job = files.jobsHead; job; job = job->next)
|
|
atomic_store_explicit(&job->cancelled, true, memory_order_release);
|
|
for (struct LocalJob * job = files.activeJobs; job; job = job->next)
|
|
{
|
|
atomic_store_explicit(&job->cancelled, true, memory_order_release);
|
|
if (job->ready)
|
|
lgSignalEvent(job->ready);
|
|
}
|
|
LG_UNLOCK(files.lock);
|
|
if (files.jobEvent)
|
|
lgSignalEvent(files.jobEvent);
|
|
for (unsigned i = 0; i < FILE_WORKERS; ++i)
|
|
if (files.workers[i])
|
|
{
|
|
lgSignalEvent(files.jobEvent);
|
|
lgJoinThread(files.workers[i], NULL);
|
|
files.workers[i] = NULL;
|
|
}
|
|
if (files.session)
|
|
fuse_session_exit(files.session);
|
|
if (files.fuseThread)
|
|
{
|
|
if (!wakeFuseThread(files.fuseStopFd))
|
|
DEBUG_WARN("Failed to wake the clipboard FUSE thread: %s",
|
|
strerror(errno));
|
|
lgJoinThread(files.fuseThread, NULL);
|
|
files.fuseThread = NULL;
|
|
}
|
|
if (files.fuseStopFd >= 0)
|
|
{
|
|
close(files.fuseStopFd);
|
|
files.fuseStopFd = -1;
|
|
}
|
|
/* Pending requests belong to the stopped but still-live session. */
|
|
freeState();
|
|
if (files.session)
|
|
{
|
|
fuse_session_unmount(files.session);
|
|
fuse_session_destroy(files.session);
|
|
files.session = NULL;
|
|
}
|
|
if (files.jobEvent)
|
|
{
|
|
lgFreeEvent(files.jobEvent);
|
|
files.jobEvent = NULL;
|
|
}
|
|
if (files.mountpoint[0] && rmdir(files.mountpoint) < 0 && errno != ENOENT)
|
|
DEBUG_WARN("Failed to remove clipboard mountpoint: %s", strerror(errno));
|
|
files.mountpoint[0] = 0;
|
|
files.initialized = false;
|
|
LG_LOCK_FREE(files.lock);
|
|
filesLockInitialized = false;
|
|
}
|