/*
 * libusbmuxd.c
 *
 * Copyright (C) 2009-2019 Nikias Bassen <nikias@gmx.li>
 * Copyright (C) 2009-2014 Martin Szulecki <m.szulecki@libimobiledevice.org>
 * Copyright (C) 2009 Paul Sladen <libiphone@paul.sladen.org>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <stdint.h>
#include <stdlib.h>
#include <errno.h>
#include <stdio.h>
#include <string.h>

#ifdef LIBUSBMUXD_STATIC
  #define USBMUXD_API
#elif defined(_WIN32)
  #define USBMUXD_API __declspec( dllexport )
#else
  #if __GNUC__ >= 4
    #define USBMUXD_API __attribute__((visibility("default")))
  #else
    #define USBMUXD_API
  #endif
#endif

#ifndef EPROTO
#define EPROTO 134
#endif
#ifndef EBADMSG
#define EBADMSG 104
#endif
#ifndef ECONNREFUSED
#define ECONNREFUSED 107
#endif

#include <unistd.h>
#include <signal.h>

#ifdef WIN32
#include <winsock2.h>
#include <windows.h>
#ifndef HAVE_SLEEP
#define sleep(x) Sleep(x*1000)
#endif
#else
#include <sys/socket.h>
#include <arpa/inet.h>
#if defined(HAVE_PROGRAM_INVOCATION_SHORT_NAME) && !defined(HAVE_PROGRAM_INVOCATION_SHORT_NAME_ERRNO_H)
extern char *program_invocation_short_name;
#endif
#ifdef __APPLE__
extern int _NSGetExecutablePath(char* buf, uint32_t* bufsize);
#include <sys/stat.h>
#endif
#endif

#ifdef HAVE_INOTIFY
#include <sys/inotify.h>
#include <sys/select.h>
#include <pthread.h>
#define EVENT_SIZE  (sizeof (struct inotify_event))
#define EVENT_BUF_LEN (1024 * (EVENT_SIZE + 16))
#define USBMUXD_DIRNAME "/var/run"
#define USBMUXD_SOCKET_NAME "usbmuxd"
static int use_inotify = 1;
#endif /* HAVE_INOTIFY */

#ifndef HAVE_STPNCPY
static char* stpncpy(char *dst, const char *src, size_t len)
{
	size_t n = strlen(src);
	if (n > len)
		n = len;
	return strncpy(dst, src, len) + n;
}
#endif

#include <plist/plist.h>
#define PLIST_CLIENT_VERSION_STRING PACKAGE_STRING
#define PLIST_LIBUSBMUX_VERSION 3

static char *bundle_id = NULL;
static char *prog_name = NULL;

// usbmuxd public interface
#include "usbmuxd.h"
// usbmuxd protocol
#include "usbmuxd-proto.h"
// socket utility functions
#include <libimobiledevice-glue/socket.h>
// misc utility functions
#include <libimobiledevice-glue/collection.h>
// threads
#include <libimobiledevice-glue/thread.h>

static int libusbmuxd_debug = 0;
#ifndef PACKAGE
#define PACKAGE "libusbmuxd"
#endif
#define LIBUSBMUXD_DEBUG(level, format, ...) if (level <= libusbmuxd_debug) fprintf(stderr, ("[" PACKAGE "] " format), __VA_ARGS__); fflush(stderr);
#define LIBUSBMUXD_ERROR(format, ...) LIBUSBMUXD_DEBUG(0, format, __VA_ARGS__)

static struct collection devices;
static THREAD_T devmon = THREAD_T_NULL;
static int listenfd = -1;
static int running = 0;
static int cancelling = 0;

static volatile int use_tag = 0;
static volatile int proto_version = 1;
static volatile int try_list_devices = 1;

struct usbmuxd_subscription_context {
	usbmuxd_event_cb_t callback;
	void *user_data;
};

static struct usbmuxd_subscription_context *event_ctx = NULL;

static struct collection listeners;
thread_once_t listener_init_once = THREAD_ONCE_INIT;
mutex_t listener_mutex;

/**
 * Finds a device info record by its handle.
 * if the record is not found, NULL is returned.
 */
static usbmuxd_device_info_t *devices_find(uint32_t handle)
{
	FOREACH(usbmuxd_device_info_t *dev, &devices) {
		if (dev && dev->handle == handle) {
			return dev;
		}
	} ENDFOREACH
	return NULL;
}

/**
 * Creates a socket connection to usbmuxd.
 * For Mac/Linux it is a unix domain socket,
 * for Windows it is a tcp socket.
 */
static int connect_usbmuxd_socket()
{
	int res = -1;
	char *usbmuxd_socket_addr = getenv("USBMUXD_SOCKET_ADDRESS");
	if (usbmuxd_socket_addr) {
		if (strncmp(usbmuxd_socket_addr, "UNIX:", 5) == 0) {
#if defined(WIN32) || defined(__CYGWIN__)
			/* not supported, ignore */
#else
			if (usbmuxd_socket_addr[5] != '\0') {
				res = socket_connect_unix(usbmuxd_socket_addr+5);
				if (res < 0) {
					res = -errno;
				}
				return res;
			}
#endif
		} else {
			uint16_t port = 0;
			char *p = strrchr(usbmuxd_socket_addr, ':');
			if (p) {
				char *endp = NULL;
				long l_port = strtol(p+1, &endp, 10);
				if (endp && *endp == '\0') {
					if (l_port > 0 && l_port < 65536) {
						port = (uint16_t)l_port;
					}
				}
			}
			if (p && port > 0) {
				char *connect_addr = NULL;
				if (usbmuxd_socket_addr[0] == '[') {
					connect_addr = strdup(usbmuxd_socket_addr+1);
					connect_addr[p - usbmuxd_socket_addr - 1] = '\0';
					p = strrchr(connect_addr, ']');
					if (p) {
						*p = '\0';
					}
				} else {
					connect_addr = strdup(usbmuxd_socket_addr);
					connect_addr[p - usbmuxd_socket_addr] = '\0';
				}
				if (connect_addr && *connect_addr != '\0') {
					res = socket_connect(connect_addr, port);
#ifdef HAVE_INOTIFY
					use_inotify = 0;
#endif
					free(connect_addr);
					if (res < 0) {
						res = -errno;
					}
					return res;
				}
				free(connect_addr);
			}
		}
	}
#if defined(WIN32) || defined(__CYGWIN__)
	res = socket_connect("127.0.0.1", USBMUXD_SOCKET_PORT);
#else
	res = socket_connect_unix(USBMUXD_SOCKET_FILE);
#endif
	if (res < 0) {
		res = -errno;
	}
	return res;
}

static void sanitize_udid(usbmuxd_device_info_t *devinfo)
{
	if (!devinfo)
		return;
	if (strlen(devinfo->udid) == 24) {
		memmove(&devinfo->udid[9], &devinfo->udid[8], 17);
		devinfo->udid[8] = '-';
	}
	if (strcasecmp(devinfo->udid, "ffffffffffffffffffffffffffffffffffffffff") == 0) {
		sprintf(devinfo->udid + 32, "%08x", devinfo->handle);
	}
}

static usbmuxd_device_info_t *device_info_from_plist(plist_t props)
{
	usbmuxd_device_info_t* devinfo = NULL;
	plist_t n = NULL;
	uint64_t val = 0;
	char *strval = NULL;

	devinfo = (usbmuxd_device_info_t*)malloc(sizeof(usbmuxd_device_info_t));
	if (!devinfo)
		return NULL;
	memset(devinfo, 0, sizeof(usbmuxd_device_info_t));

	n = plist_dict_get_item(props, "DeviceID");
	if (n && plist_get_node_type(n) == PLIST_UINT) {
		plist_get_uint_val(n, &val);
		devinfo->handle = (uint32_t)val;
	}

	n = plist_dict_get_item(props, "ProductID");
	if (n && plist_get_node_type(n) == PLIST_UINT) {
		plist_get_uint_val(n, &val);
		devinfo->product_id = (uint32_t)val;
	}

	n = plist_dict_get_item(props, "SerialNumber");
	if (n && plist_get_node_type(n) == PLIST_STRING) {
		plist_get_string_val(n, &strval);
		if (strval) {
			char *t = stpncpy(devinfo->udid, strval, sizeof(devinfo->udid)-1);
			*t = '\0';
			sanitize_udid(devinfo);
			free(strval);
		}
	}

	n = plist_dict_get_item(props, "ConnectionType");
	if (n && plist_get_node_type(n) == PLIST_STRING) {
		plist_get_string_val(n, &strval);
		if (strval) {
			if (strcmp(strval, "USB") == 0) {
				devinfo->conn_type = CONNECTION_TYPE_USB;
			} else if (strcmp(strval, "Network") == 0) {
				devinfo->conn_type = CONNECTION_TYPE_NETWORK;
				n = plist_dict_get_item(props, "NetworkAddress");
				if (n && plist_get_node_type(n) == PLIST_DATA) {
					uint64_t addr_len = 0;
					const char *netaddr = plist_get_data_ptr(n, &addr_len);
					if (netaddr && addr_len > 0 && addr_len < sizeof(devinfo->conn_data)) {
						memcpy(devinfo->conn_data, netaddr, addr_len);
					}
				}
			} else {
				LIBUSBMUXD_ERROR("%s: Unexpected ConnectionType '%s'\n", __func__, strval);
			}
			free(strval);
		}
	}

	if (!devinfo->udid[0]) {
		LIBUSBMUXD_ERROR("%s: Failed to get SerialNumber (UDID)!\n", __func__);
		free(devinfo);
		return NULL;
	}
	if (!devinfo->conn_type) {
		LIBUSBMUXD_ERROR("%s: Failed to get ConnectionType!\n", __func__);
		free(devinfo);
		devinfo = NULL;
	} else if (devinfo->conn_type == CONNECTION_TYPE_NETWORK && !devinfo->conn_data[0]) {
		LIBUSBMUXD_ERROR("%s: Failed to get EscapedFullServiceName!\n", __func__);
		free(devinfo);
		devinfo = NULL;
	}

	return devinfo;
}

static usbmuxd_device_info_t *device_info_from_device_record(struct usbmuxd_device_record *dev)
{
	if (!dev) {
		return NULL;
	}
	usbmuxd_device_info_t *devinfo = (usbmuxd_device_info_t*)malloc(sizeof(usbmuxd_device_info_t));
	if (!devinfo) {
		LIBUSBMUXD_ERROR("%s: Out of memory while allocating device info object\n", __func__);
		return NULL;
	}

	devinfo->handle = dev->device_id;
	devinfo->product_id = dev->product_id;
	char *t = stpncpy(devinfo->udid, dev->serial_number, sizeof(devinfo->udid)-2);
	*t = '\0';
	sanitize_udid(devinfo);

	return devinfo;
}

static int receive_packet(int sfd, struct usbmuxd_header *header, void **payload, int timeout)
{
	int recv_len;
	struct usbmuxd_header hdr;
	char *payload_loc = NULL;

	header->length = 0;
	header->version = 0;
	header->message = 0;
	header->tag = 0;

	recv_len = socket_receive_timeout(sfd, &hdr, sizeof(hdr), 0, timeout);
	if (recv_len < 0) {
		if (!cancelling) {
			LIBUSBMUXD_DEBUG(1, "%s: Error receiving packet: %s\n", __func__, strerror(-recv_len));
		}
		return recv_len;
	}

	if ((size_t)recv_len < sizeof(hdr)) {
		LIBUSBMUXD_DEBUG(1, "%s: Received packet is too small, got %d bytes!\n", __func__, recv_len);
		return recv_len;
	}

	uint32_t payload_size = hdr.length - sizeof(hdr);
	if (payload_size > 0) {
		payload_loc = (char*)malloc(payload_size);
		uint32_t rsize = 0;
		do {
			int res = socket_receive_timeout(sfd, payload_loc + rsize, payload_size - rsize, 0, 5000);
			if (res < 0) {
				break;
			}
			rsize += res;
		} while (rsize < payload_size);
		if (rsize != payload_size) {
			LIBUSBMUXD_DEBUG(1, "%s: Error receiving payload of size %d (bytes received: %d)\n", __func__, payload_size, rsize);
			free(payload_loc);
			return -EBADMSG;
		}
	}

	if (hdr.message == MESSAGE_PLIST) {
		char *message = NULL;
		plist_t plist = NULL;
		plist_from_xml(payload_loc, payload_size, &plist);
		free(payload_loc);

		if (!plist) {
			LIBUSBMUXD_DEBUG(1, "%s: Error getting plist from payload!\n", __func__);
			return -EBADMSG;
		}

		plist_t node = plist_dict_get_item(plist, "MessageType");
		if (!node || plist_get_node_type(node) != PLIST_STRING) {
			*payload = plist;
			hdr.length = sizeof(hdr);
			memcpy(header, &hdr, sizeof(hdr));
			return hdr.length;
		}

		plist_get_string_val(node, &message);
		if (message) {
			uint64_t val = 0;
			if (strcmp(message, "Result") == 0) {
				/* result message */
				uint32_t dwval = 0;
				plist_t n = plist_dict_get_item(plist, "Number");
				plist_get_uint_val(n, &val);
				*payload = malloc(sizeof(uint32_t));
				dwval = val;
				memcpy(*payload, &dwval, sizeof(dwval));
				hdr.length = sizeof(hdr) + sizeof(dwval);
				hdr.message = MESSAGE_RESULT;
			} else if (strcmp(message, "Attached") == 0) {
				/* device add message */
				usbmuxd_device_info_t *devinfo = NULL;
				plist_t props = plist_dict_get_item(plist, "Properties");
				if (!props) {
					LIBUSBMUXD_DEBUG(1, "%s: Could not get properties for message '%s' from plist!\n", __func__, message);
					free(message);
					plist_free(plist);
					return -EBADMSG;
				}

				devinfo = device_info_from_plist(props);
				if (!devinfo) {
					LIBUSBMUXD_DEBUG(1, "%s: Could not create device info object from properties!\n", __func__);
					free(message);
					plist_free(plist);
					return -EBADMSG;
				}
				*payload = (void*)devinfo;
				hdr.length = sizeof(hdr) + sizeof(usbmuxd_device_info_t);
				hdr.message = MESSAGE_DEVICE_ADD;
			} else if (strcmp(message, "Detached") == 0) {
				/* device remove message */
				uint32_t dwval = 0;
				plist_t n = plist_dict_get_item(plist, "DeviceID");
				if (n) {
					plist_get_uint_val(n, &val);
					*payload = malloc(sizeof(uint32_t));
					dwval = val;
					memcpy(*payload, &dwval, sizeof(dwval));
					hdr.length = sizeof(hdr) + sizeof(dwval);
					hdr.message = MESSAGE_DEVICE_REMOVE;
				}
			} else if (strcmp(message, "Paired") == 0) {
				/* device pair message */
				uint32_t dwval = 0;
				plist_t n = plist_dict_get_item(plist, "DeviceID");
				if (n) {
					plist_get_uint_val(n, &val);
					*payload = malloc(sizeof(uint32_t));
					dwval = val;
					memcpy(*payload, &dwval, sizeof(dwval));
					hdr.length = sizeof(hdr) + sizeof(dwval);
					hdr.message = MESSAGE_DEVICE_PAIRED;
				}
			} else {
				char *xml = NULL;
				uint32_t len = 0;
				plist_to_xml(plist, &xml, &len);
				LIBUSBMUXD_DEBUG(1, "%s: Unexpected message '%s' in plist:\n%s\n", __func__, message, xml);
				free(xml);
				free(message);
				plist_free(plist);
				return -EBADMSG;
			}
			free(message);
		}
		plist_free(plist);
	} else if (hdr.message == MESSAGE_DEVICE_ADD) {
		usbmuxd_device_info_t *devinfo = device_info_from_device_record((struct usbmuxd_device_record*)payload_loc);
		free(payload_loc);
		*payload = devinfo;
	} else {
		*payload = payload_loc;
	}

	memcpy(header, &hdr, sizeof(hdr));

	return hdr.length;
}

/**
 * Retrieves the result code to a previously sent request.
 */
static int usbmuxd_get_result(int sfd, uint32_t tag, uint32_t *result, void **result_plist)
{
	struct usbmuxd_header hdr;
	int recv_len;
	uint32_t *res = NULL;

	if (!result) {
		return -EINVAL;
	}
	*result = -1;
	if (result_plist) {
		*result_plist = NULL;
	}

	recv_len = receive_packet(sfd, &hdr, (void**)&res, 5000);
	if (recv_len < 0 || (size_t)recv_len < sizeof(hdr)) {
		free(res);
		return (recv_len < 0 ? recv_len : -EPROTO);
	}

	if (hdr.message == MESSAGE_RESULT) {
		int ret = 0;
		if (hdr.tag != tag) {
			LIBUSBMUXD_DEBUG(1, "%s: WARNING: tag mismatch (%d != %d). Proceeding anyway.\n", __func__, hdr.tag, tag);
		}
		if (res) {
			memcpy(result, res, sizeof(uint32_t));
			ret = 1;
		}
		free(res);
		return ret;
	}

	if (hdr.message == MESSAGE_PLIST) {
		if (!result_plist) {
			LIBUSBMUXD_DEBUG(1, "%s: MESSAGE_PLIST result but result_plist pointer is NULL!\n", __func__);
			return -1;
		}
		*result_plist = (plist_t)res;
		*result = RESULT_OK;
		return 1;
	}

	LIBUSBMUXD_DEBUG(1, "%s: Unexpected message of type %d received!\n", __func__, hdr.message);
	free(res);
	return -EPROTO;
}

static int send_packet(int sfd, uint32_t message, uint32_t tag, void *payload, uint32_t payload_size)
{
	struct usbmuxd_header header;

	header.length = sizeof(struct usbmuxd_header);
	header.version = proto_version;
	header.message = message;
	header.tag = tag;
	if (payload && (payload_size > 0)) {
		header.length += payload_size;
	}
	int sent = socket_send(sfd, &header, sizeof(header));
	if (sent != sizeof(header)) {
		LIBUSBMUXD_DEBUG(1, "%s: ERROR: could not send packet header\n", __func__);
		return -1;
	}
	if (payload && (payload_size > 0)) {
		uint32_t ssize = 0;
		do {
			int res = socket_send(sfd, (char*)payload + ssize, payload_size - ssize);
			if (res < 0) {
				break;
			}
			ssize += res;
		} while (ssize < payload_size);
		sent += ssize;
	}
	if (sent != (int)header.length) {
		LIBUSBMUXD_DEBUG(1, "%s: ERROR: could not send whole packet (sent %d of %d)\n", __func__, sent, header.length);
		socket_close(sfd);
		return -1;
	}
	return sent;
}

static int send_plist_packet(int sfd, uint32_t tag, plist_t message)
{
	int res;
	char *payload = NULL;
	uint32_t payload_size = 0;

	plist_to_xml(message, &payload, &payload_size);
	res = send_packet(sfd, MESSAGE_PLIST, tag, payload, payload_size);
	free(payload);

	return res;
}

static void get_bundle_id()
{
#if defined (__APPLE__)
	char CONTENTS_INFO_PLIST[] = "Contents/Info.plist";
	char* execpath = malloc(1024);
	uint32_t size = 1024;
	if (_NSGetExecutablePath(execpath, &size) != 0) {
		free(execpath);
		return;
	}
	// strip off executable name
	char *p = execpath + strlen(execpath) - 1;
	while (p > execpath && *p != '/') p--;
	if (*p == '/') *p = '\0';
	// now walk back trying to find "/Contents/MacOS", and strip it off
	int macos_found = 0;
	while (p > execpath) {
		p--;
		if (*p != '/') continue;
		if (strcmp(p, "/.") == 0) {
			*p = '\0';
		} else if (!macos_found && strcmp(p, "/MacOS") == 0) {
			*p = '\0';
			macos_found++;
		} else if (macos_found && strcmp(p, "/Contents") == 0) {
			*p = '\0';
			break;
		} else {
			break;
		}
	}
	// now just append "/Contents/Info.plist"
	size_t len = strlen(execpath) + sizeof(CONTENTS_INFO_PLIST) + 1;
	char *infopl = malloc(len);
	snprintf(infopl, len, "%s/%s", execpath, CONTENTS_INFO_PLIST);
	free(execpath);
	struct stat fst;
	fst.st_size = 0;
	if (stat(infopl, &fst) != 0) {
		free(infopl);
		return;
	}
	size_t fsize = fst.st_size;
	if (fsize < 8) {
		free(infopl);
		return;
	}
	FILE *f = fopen(infopl, "r");
	free(infopl);
	if (!f)
		return;
	char *buf = malloc(fsize);
	if (!buf)
		return;
	if (fread(buf, 1, fsize, f) == fsize) {
		plist_t pl = NULL;
		if (memcmp(buf, "bplist00", 8) == 0) {
			plist_from_bin(buf, fst.st_size, &pl);
		} else {
			plist_from_xml(buf, fst.st_size, &pl);
		}
		if (pl) {
			plist_t bid = plist_dict_get_item(pl, "CFBundleIdentifier");
			if (plist_get_node_type(bid) == PLIST_STRING) {
				plist_get_string_val(bid, &bundle_id);
			}
			plist_free(pl);
		}
	}
	free(buf);
	fclose(f);
#endif
}

static void get_prog_name()
{
#if defined(__APPLE__) || defined(__FreeBSD__)
	const char *pname = getprogname();
	if (pname) {
		prog_name = strdup(pname);
	}
#elif defined (WIN32)
	TCHAR *_pname = malloc((MAX_PATH+1) * sizeof(TCHAR));
	if (GetModuleFileName(NULL, _pname, MAX_PATH+1) > 0) {
		char* pname = NULL;
	#if defined(UNICODE) || defined(_UNICODE)
		char* __pname = NULL;
		int l = WideCharToMultiByte(CP_UTF8, 0, _pname, -1, NULL, 0, NULL, NULL);
		if (l > 0) {
			__pname = malloc(l);
			if (WideCharToMultiByte(CP_UTF8, 0, _pname, -1, __pname, l, NULL, NULL) > 0) {
				pname = __pname;
			}
		}
	#else
		pname = _pname;
	#endif
		if (pname) {
			char *p = pname+strlen(pname)-1;
			while (p > pname && *p != '\\' && *p != '/') p--;
			if (*p == '\\' || *p == '/') p++;
			prog_name = strdup(p);
		}
	#if defined(UNICODE) || defined(_UNICODE)
		free(__pname);
	#endif
	}
	free(_pname);
#elif defined (HAVE_PROGRAM_INVOCATION_SHORT_NAME)
	char *pname = program_invocation_short_name;
	if (pname) {
		prog_name = strdup(pname);
	}
#elif defined (__linux__)
	FILE *f = fopen("/proc/self/stat", "r");
	if (!f) {
		return;
	}
	char *tmpbuf = malloc(512);
	size_t r = fread(tmpbuf, 1, 512, f);
	if (r > 0) {
		char *p = tmpbuf;
		while (((size_t)(p-tmpbuf) < r) && (*p != '(') && (*p != '\0')) p++;
		if (*p == '(') {
			p++;
			char *pname = p;
			while (((size_t)(p-tmpbuf) < r) && (*p != ')') && (*p != '\0')) p++;
			if (*p == ')') {
				*p = '\0';
				prog_name = strdup(pname);
			}
		}
	}
	free(tmpbuf);
	fclose(f);
#else
	#warning FIXME: no method to determine program name
#endif
}

static plist_t create_plist_message(const char* message_type)
{
	if (!bundle_id) {
		get_bundle_id();
	}
	if (!prog_name) {
		get_prog_name();
	}
	plist_t plist = plist_new_dict();
	if (bundle_id) {
		plist_dict_set_item(plist, "BundleID", plist_new_string(bundle_id));
	}
	plist_dict_set_item(plist, "ClientVersionString", plist_new_string(PLIST_CLIENT_VERSION_STRING));
	plist_dict_set_item(plist, "MessageType", plist_new_string(message_type));
	if (prog_name) {
		plist_dict_set_item(plist, "ProgName", plist_new_string(prog_name));
	}
	plist_dict_set_item(plist, "kLibUSBMuxVersion", plist_new_uint(PLIST_LIBUSBMUX_VERSION));
	return plist;
}

static int send_listen_packet(int sfd, uint32_t tag)
{
	int res = 0;
	if (proto_version == 1) {
		/* construct message plist */
		plist_t plist = create_plist_message("Listen");

		res = send_plist_packet(sfd, tag, plist);
		plist_free(plist);
	} else {
		/* binary packet */
		res = send_packet(sfd, MESSAGE_LISTEN, tag, NULL, 0);
	}
	return res;
}

static int send_connect_packet(int sfd, uint32_t tag, uint32_t device_id, uint16_t port)
{
	int res = 0;
	if (proto_version == 1) {
		/* construct message plist */
		plist_t plist = create_plist_message("Connect");
		plist_dict_set_item(plist, "DeviceID", plist_new_uint(device_id));
		plist_dict_set_item(plist, "PortNumber", plist_new_uint(htons(port)));

		res = send_plist_packet(sfd, tag, plist);
		plist_free(plist);
	} else {
		/* binary packet */
		struct {
			uint32_t device_id;
			uint16_t port;
			uint16_t reserved;
		} conninfo;

		conninfo.device_id = device_id;
		conninfo.port = htons(port);
		conninfo.reserved = 0;

		res = send_packet(sfd, MESSAGE_CONNECT, tag, &conninfo, sizeof(conninfo));
	}
	return res;
}

static int send_list_devices_packet(int sfd, uint32_t tag)
{
	int res = -1;

	/* construct message plist */
	plist_t plist = create_plist_message("ListDevices");

	res = send_plist_packet(sfd, tag, plist);
	plist_free(plist);

	return res;
}

static int send_read_buid_packet(int sfd, uint32_t tag)
{
	int res = -1;

	/* construct message plist */
	plist_t plist = create_plist_message("ReadBUID");

	res = send_plist_packet(sfd, tag, plist);
	plist_free(plist);

	return res;
}

static int send_pair_record_packet(int sfd, uint32_t tag, const char* msgtype, const char* pair_record_id, uint32_t device_id, plist_t data)
{
	int res = -1;

	/* construct message plist */
	plist_t plist = create_plist_message(msgtype);
	plist_dict_set_item(plist, "PairRecordID", plist_new_string(pair_record_id));
	if (data) {
		plist_dict_set_item(plist, "PairRecordData", plist_copy(data));
	}
	if (device_id > 0) {
		plist_dict_set_item(plist, "DeviceID", plist_new_uint(device_id));
	}

	res = send_plist_packet(sfd, tag, plist);
	plist_free(plist);

	return res;
}

/**
 * Generates an event, i.e. calls the callback function.
 * A reference to a populated usbmuxd_event_t with information about the event
 * and the corresponding device will be passed to the callback function.
 */
static void generate_event(const usbmuxd_device_info_t *dev, enum usbmuxd_event_type event)
{
	usbmuxd_event_t ev;

	if (!dev) {
		return;
	}

	ev.event = event;
	memcpy(&ev.device, dev, sizeof(usbmuxd_device_info_t));

	mutex_lock(&listener_mutex);
	FOREACH(struct usbmuxd_subscription_context* context, &listeners) {
		context->callback(&ev, context->user_data);
	} ENDFOREACH
	mutex_unlock(&listener_mutex);
}

static int usbmuxd_listen_poll()
{
	int sfd;

	sfd = connect_usbmuxd_socket();
	if (sfd < 0) {
		while (1) {
			mutex_lock(&listener_mutex);
			int num = collection_count(&listeners);
			mutex_unlock(&listener_mutex);
			if (num <= 0) {
				break;
			}
			if ((sfd = connect_usbmuxd_socket()) >= 0) {
				break;
			}
			sleep(1);
		}
	}

	return sfd;
}

#ifdef HAVE_INOTIFY
#ifndef HAVE_PSELECT
static int pselect(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, const struct timespec *timeout, const sigset_t *sigmask)
{
	int ready;
	struct timeval tv;
	struct timeval *p_timeout;
	sigset_t origmask;

	if (timeout) {
		tv.tv_sec = timeout->tv_sec;
		tv.tv_usec = timeout->tv_nsec / 1000;
		p_timeout = &tv;
	} else {
		p_timeout = NULL;
	}

	pthread_sigmask(SIG_SETMASK, sigmask, &origmask);
	ready = select(nfds, readfds, writefds, exceptfds, p_timeout);
	pthread_sigmask(SIG_SETMASK, &origmask, NULL);

	return ready;
}
#endif

static int usbmuxd_listen_inotify()
{
	int inot_fd;
	int watch_d;
	int sfd;

	if (!use_inotify) {
		return -2;
	}

	sfd = connect_usbmuxd_socket();
	if (sfd >= 0)
		return sfd;

	sfd = -1;
	inot_fd = inotify_init1(IN_CLOEXEC);
	if (inot_fd < 0) {
		LIBUSBMUXD_DEBUG(1, "%s: Failed to setup inotify\n", __func__);
		return -2;
	}

	/* inotify is setup, listen for events that concern us */
	watch_d = inotify_add_watch (inot_fd, USBMUXD_DIRNAME, IN_CREATE);
	if (watch_d < 0) {
		LIBUSBMUXD_DEBUG(1, "%s: Failed to setup watch descriptor for socket dir\n", __func__);
		close (inot_fd);
		return -2;
	}

	while (1) {
		fd_set rfds;
		struct timespec tv = {1, 0};

		FD_ZERO(&rfds);
		FD_SET(inot_fd, &rfds);
		int r = pselect(inot_fd+1, &rfds, NULL, NULL, &tv, NULL);
		if (r < 0) {
			break;
		}

		if (r == 0) {
			continue;
		}

		ssize_t len, i;
		char buff[EVENT_BUF_LEN] = {0};

		i = 0;
		len = read (inot_fd, buff, EVENT_BUF_LEN -1);
		if (len < 0)
			goto end;
		while (i < len) {
			struct inotify_event *pevent = (struct inotify_event *) & buff[i];

			/* check that it's ours */
			if (pevent->mask & IN_CREATE &&
				pevent->len &&
				pevent->name[0] != 0 &&
				strcmp(pevent->name, USBMUXD_SOCKET_NAME) == 0) {
				/* retry if usbmuxd isn't ready yet */
				int retry = 10;
				while (--retry >= 0) {
					if ((sfd = connect_usbmuxd_socket ()) >= 0) {
						break;
					}
					sleep(1);
				}
				goto end;
			}
			i += EVENT_SIZE + pevent->len;
		}
	}

end:
	inotify_rm_watch(inot_fd, watch_d);
	close(inot_fd);

	return sfd;
}
#endif /* HAVE_INOTIFY */

/**
 * Tries to connect to usbmuxd and wait if it is not running.
 */
static int usbmuxd_listen()
{
	int sfd;
	uint32_t res = -1;
	int tag;

retry:

#ifdef HAVE_INOTIFY
	sfd = usbmuxd_listen_inotify();
	if (sfd == -2)
		sfd = usbmuxd_listen_poll();
#else
	sfd = usbmuxd_listen_poll();
#endif

	if (sfd < 0) {
		if (!cancelling) {
			LIBUSBMUXD_DEBUG(1, "%s: ERROR: usbmuxd was supposed to be running here...\n", __func__);
		}
		return sfd;
	}

	tag = ++use_tag;
	if (send_listen_packet(sfd, tag) <= 0) {
		LIBUSBMUXD_DEBUG(1, "%s: ERROR: could not send listen packet\n", __func__);
		socket_close(sfd);
		return -1;
	}
	if ((usbmuxd_get_result(sfd, tag, &res, NULL) == 1) && (res != 0)) {
		socket_close(sfd);
		if ((res == RESULT_BADVERSION) && (proto_version == 1)) {
			proto_version = 0;
			goto retry;
		}
		LIBUSBMUXD_DEBUG(1, "%s: ERROR: did not get OK but %d\n", __func__, res);
		return -1;
	}
	return sfd;
}

/**
 * Waits for an event to occur, i.e. a packet coming from usbmuxd.
 * Calls generate_event to pass the event via callback to the client program.
 */
static int get_next_event(int sfd)
{
	struct usbmuxd_header hdr;
	void *payload = NULL;

	/* block until we receive something */
	if (receive_packet(sfd, &hdr, &payload, 0) < 0) {
		if (!cancelling) {
			LIBUSBMUXD_DEBUG(1, "%s: Error in usbmuxd connection, disconnecting all devices!\n", __func__);
		}
		// when then usbmuxd connection fails,
		// generate remove events for every device that
		// is still present so applications know about it
		FOREACH(usbmuxd_device_info_t *dev, &devices) {
			generate_event(dev, UE_DEVICE_REMOVE);
			collection_remove(&devices, dev);
			free(dev);
		} ENDFOREACH
		return -EIO;
	}

	if ((hdr.length > sizeof(hdr)) && !payload) {
		LIBUSBMUXD_DEBUG(1, "%s: Invalid packet received, payload is missing!\n", __func__);
		return -EBADMSG;
	}

	if (hdr.message == MESSAGE_DEVICE_ADD) {
		usbmuxd_device_info_t *devinfo = (usbmuxd_device_info_t*)payload;
		collection_add(&devices, devinfo);
		generate_event(devinfo, UE_DEVICE_ADD);
		payload = NULL;
	} else if (hdr.message == MESSAGE_DEVICE_REMOVE) {
		uint32_t handle;
		usbmuxd_device_info_t *devinfo;

		memcpy(&handle, payload, sizeof(uint32_t));

		devinfo = devices_find(handle);
		if (!devinfo) {
			LIBUSBMUXD_DEBUG(1, "%s: WARNING: got device remove message for handle %d, but couldn't find the corresponding handle in the device list. This event will be ignored.\n", __func__, handle);
		} else {
			generate_event(devinfo, UE_DEVICE_REMOVE);
			collection_remove(&devices, devinfo);
			free(devinfo);
		}
	} else if (hdr.message == MESSAGE_DEVICE_PAIRED) {
		uint32_t handle;
		usbmuxd_device_info_t *devinfo;

		memcpy(&handle, payload, sizeof(uint32_t));

		devinfo = devices_find(handle);
		if (!devinfo) {
			LIBUSBMUXD_DEBUG(1, "%s: WARNING: got paired message for device handle %d, but couldn't find the corresponding handle in the device list. This event will be ignored.\n", __func__, handle);
		} else {
			generate_event(devinfo, UE_DEVICE_PAIRED);
		}
	} else if (hdr.length > 0) {
		LIBUSBMUXD_DEBUG(1, "%s: Unexpected message type %d length %d received!\n", __func__, hdr.message, hdr.length);
	}
	free(payload);
	return 0;
}

static void device_monitor_cleanup(void* data)
{
	FOREACH(usbmuxd_device_info_t *dev, &devices) {
		collection_remove(&devices, dev);
		free(dev);
	} ENDFOREACH
	collection_free(&devices);

	socket_close(listenfd);
	listenfd = -1;
}

/**
 * Device Monitor thread function.
 *
 * This function sets up a connection to usbmuxd
 */
static void *device_monitor(void *data)
{
	running = 1;
	collection_init(&devices);
	cancelling = 0;

#ifdef HAVE_THREAD_CLEANUP
	thread_cleanup_push(device_monitor_cleanup, NULL);
#endif
	do {

		listenfd = usbmuxd_listen();
		if (listenfd < 0) {
			continue;
		}

		while (running) {
			int res = get_next_event(listenfd);
			if (res < 0) {
				break;
			}
		}

		mutex_lock(&listener_mutex);
		if (collection_count(&listeners) == 0) {
			running = 0;
		}
		mutex_unlock(&listener_mutex);
	} while (running);

#ifdef HAVE_THREAD_CLEANUP
	thread_cleanup_pop(1);
#else
	device_monitor_cleanup(NULL);
#endif

	return NULL;
}

static void init_listeners(void)
{
	collection_init(&listeners);
	mutex_init(&listener_mutex);
}

int usbmuxd_events_subscribe(usbmuxd_subscription_context_t *context, usbmuxd_event_cb_t callback, void *user_data)
{
	if (!context || !callback) {
		return -EINVAL;
	}

	thread_once(&listener_init_once, init_listeners);

	mutex_lock(&listener_mutex);
	*context = malloc(sizeof(struct usbmuxd_subscription_context));
	if (!*context) {
		mutex_unlock(&listener_mutex);
		LIBUSBMUXD_ERROR("ERROR: %s: malloc failed\n", __func__);
		return -ENOMEM;
	}
	(*context)->callback = callback;
	(*context)->user_data = user_data;

	collection_add(&listeners, *context);

	if (devmon == THREAD_T_NULL || !thread_alive(devmon)) {
		mutex_unlock(&listener_mutex);
		int res = thread_new(&devmon, device_monitor, NULL);
		if (res != 0) {
			free(*context);
			LIBUSBMUXD_DEBUG(1, "%s: ERROR: Could not start device watcher thread!\n", __func__);
			return res;
		}
	} else {
		/* we need to submit DEVICE_ADD events to the new listener */
		FOREACH(usbmuxd_device_info_t *dev, &devices) {
			if (dev) {
				usbmuxd_event_t ev;
				ev.event = UE_DEVICE_ADD;
				memcpy(&ev.device, dev, sizeof(usbmuxd_device_info_t));
				(*context)->callback(&ev, (*context)->user_data);
			}
		} ENDFOREACH
		mutex_unlock(&listener_mutex);
	}

	return 0;
}

int usbmuxd_events_unsubscribe(usbmuxd_subscription_context_t context)
{
	int ret = 0;
	int num = 0;

	if (!context) {
		return -EINVAL;
	}

	mutex_lock(&listener_mutex);
	if (collection_remove(&listeners, context) == 0) {
		FOREACH(usbmuxd_device_info_t *dev, &devices) {
			if (dev) {
				usbmuxd_event_t ev;
				ev.event = UE_DEVICE_REMOVE;
				memcpy(&ev.device, dev, sizeof(usbmuxd_device_info_t));
				(context)->callback(&ev, (context)->user_data);
			}
		} ENDFOREACH
		free(context);
	}
	num = collection_count(&listeners);
	mutex_unlock(&listener_mutex);

	if (num == 0) {
		int res = 0;
		cancelling = 1;
		socket_shutdown(listenfd, SHUT_RDWR);
		if (thread_alive(devmon)) {
			if (thread_cancel(devmon) < 0) {
				running = 0;
			}
#if defined(HAVE_INOTIFY) && !defined(HAVE_PTHREAD_CANCEL)
			pthread_kill(devmon, SIGINT);
#endif
			res = thread_join(devmon);
			thread_free(devmon);
			devmon = THREAD_T_NULL;
		}
		if ((res != 0) && (res != ESRCH)) {
			ret = res;
		}
	}

	return ret;
}

int usbmuxd_subscribe(usbmuxd_event_cb_t callback, void *user_data)
{
	if (!callback) {
		return -EINVAL;
	}

	if (event_ctx) {
		usbmuxd_events_unsubscribe(event_ctx);
		event_ctx = NULL;
	}
	return usbmuxd_events_subscribe(&event_ctx, callback, user_data);
}

int usbmuxd_unsubscribe(void)
{
	int res = usbmuxd_events_unsubscribe(event_ctx);
	event_ctx = NULL;
	return res;
}

int usbmuxd_get_device_list(usbmuxd_device_info_t **device_list)
{
	int sfd;
	int tag;
	int listen_success = 0;
	uint32_t res;
	struct collection tmpdevs;
	usbmuxd_device_info_t *newlist = NULL;
	struct usbmuxd_header hdr;
	int dev_cnt = 0;
	void *payload = NULL;

	*device_list = NULL;

retry:
	sfd = connect_usbmuxd_socket();
	if (sfd < 0) {
		LIBUSBMUXD_DEBUG(1, "%s: error opening socket!\n", __func__);
		return sfd;
	}

	tag = ++use_tag;
	if ((proto_version == 1) && (try_list_devices)) {
		if (send_list_devices_packet(sfd, tag) > 0) {
			plist_t list = NULL;
			if ((usbmuxd_get_result(sfd, tag, &res, &list) == 1) && (res == 0)) {
				plist_t devlist = plist_dict_get_item(list, "DeviceList");
				if (devlist && plist_get_node_type(devlist) == PLIST_ARRAY) {
					collection_init(&tmpdevs);
					uint32_t numdevs = plist_array_get_size(devlist);
					uint32_t i;
					for (i = 0; i < numdevs; i++) {
						plist_t pdev = plist_array_get_item(devlist, i);
						plist_t props = plist_dict_get_item(pdev, "Properties");
						usbmuxd_device_info_t *devinfo = device_info_from_plist(props);
						if (!devinfo) {
							socket_close(sfd);
							LIBUSBMUXD_DEBUG(1, "%s: Could not create device info object from properties!\n", __func__);
							plist_free(list);
							return -1;
						}
						collection_add(&tmpdevs, devinfo);
					}
					plist_free(list);
					goto got_device_list;
				}
			} else {
				if (res == RESULT_BADVERSION) {
					proto_version = 0;
				}
				socket_close(sfd);
				try_list_devices = 0;
				plist_free(list);
				goto retry;
			}
			plist_free(list);
		}
	}

	tag = ++use_tag;
	if (send_listen_packet(sfd, tag) > 0) {
		res = -1;
		// get response
		if ((usbmuxd_get_result(sfd, tag, &res, NULL) == 1) && (res == 0)) {
			listen_success = 1;
		} else {
			socket_close(sfd);
			if ((res == RESULT_BADVERSION) && (proto_version == 1)) {
				proto_version = 0;
				goto retry;
			}
			LIBUSBMUXD_DEBUG(1, "%s: Did not get response to scan request (with result=0)...\n", __func__);
			return res;
		}
	}

	if (!listen_success) {
		socket_close(sfd);
		LIBUSBMUXD_DEBUG(1, "%s: Could not send listen request!\n", __func__);
		return -1;
	}

	collection_init(&tmpdevs);

	// receive device list
	while (1) {
		if (receive_packet(sfd, &hdr, &payload, 100) > 0) {
			if (hdr.message == MESSAGE_DEVICE_ADD) {
				usbmuxd_device_info_t *devinfo = payload;
				collection_add(&tmpdevs, devinfo);
				payload = NULL;
			} else if (hdr.message == MESSAGE_DEVICE_REMOVE) {
				uint32_t handle;
				usbmuxd_device_info_t *devinfo = NULL;

				memcpy(&handle, payload, sizeof(uint32_t));

				FOREACH(usbmuxd_device_info_t *di, &tmpdevs) {
					if (di && di->handle == handle) {
						devinfo = di;
						break;
					}
				} ENDFOREACH
				if (devinfo) {
					collection_remove(&tmpdevs, devinfo);
					free(devinfo);
				}
			} else {
				LIBUSBMUXD_DEBUG(1, "%s: Unexpected message %d\n", __func__, hdr.message);
			}
			free(payload);
		} else {
			// we _should_ have all of them now.
			// or perhaps an error occurred.
			break;
		}
	}

got_device_list:

	// explicitly close connection
	socket_close(sfd);

	// create copy of device info entries from collection
	newlist = (usbmuxd_device_info_t*)malloc(sizeof(usbmuxd_device_info_t) * (collection_count(&tmpdevs) + 1));
	dev_cnt = 0;
	FOREACH(usbmuxd_device_info_t *di, &tmpdevs) {
		if (di) {
			memcpy(&newlist[dev_cnt], di, sizeof(usbmuxd_device_info_t));
			free(di);
			dev_cnt++;
		}
	} ENDFOREACH
	collection_free(&tmpdevs);

	memset(&newlist[dev_cnt], 0, sizeof(usbmuxd_device_info_t));
	*device_list = newlist;

	return dev_cnt;
}

int usbmuxd_device_list_free(usbmuxd_device_info_t **device_list)
{
	if (device_list) {
		free(*device_list);
	}
	return 0;
}

int usbmuxd_get_device_by_udid(const char *udid, usbmuxd_device_info_t *device)
{
	usbmuxd_device_info_t *dev_list = NULL;
	usbmuxd_device_info_t *dev = NULL;
	int result = 0;
	int i;

	if (!device) {
		return -EINVAL;
	}
	if (usbmuxd_get_device_list(&dev_list) < 0) {
		return -ENODEV;
	}

	for (i = 0; dev_list[i].handle > 0; i++) {
	 	if (!udid) {
			if (dev_list[i].conn_type == CONNECTION_TYPE_USB) {
				dev = &dev_list[i];
				break;
			}
		} else if (!strcmp(udid, dev_list[i].udid)) {
			if (dev_list[i].conn_type == CONNECTION_TYPE_USB) {
				dev = &dev_list[i];
				break;
			}
		}
	}

	if (dev) {
		device->handle = dev->handle;
		device->product_id = dev->product_id;
		char *t = stpncpy(device->udid, dev->udid, sizeof(device->udid)-1);
		*t = '\0';
		device->conn_type = dev->conn_type;
		memcpy(device->conn_data, dev->conn_data, sizeof(device->conn_data));
		result = 1;
	}

	usbmuxd_device_list_free(&dev_list);

	return result;
}

int usbmuxd_get_device(const char *udid, usbmuxd_device_info_t *device, enum usbmux_lookup_options options)
{
	usbmuxd_device_info_t *dev_list = NULL;
	usbmuxd_device_info_t *dev_network = NULL;
	usbmuxd_device_info_t *dev_usbmuxd = NULL;
	usbmuxd_device_info_t *dev = NULL;
	int result = 0;
	int i;

	if (!device) {
		return -EINVAL;
	}
	if (usbmuxd_get_device_list(&dev_list) < 0) {
		return -ENODEV;
	}

	if (options == 0) {
		options = DEVICE_LOOKUP_USBMUX;
	}

	for (i = 0; dev_list[i].handle > 0; i++) {
		if (!udid) {
			if ((options & DEVICE_LOOKUP_USBMUX) && (dev_list[i].conn_type == CONNECTION_TYPE_USB)) {
				dev_usbmuxd = &dev_list[i];
				break;
			}
			if ((options & DEVICE_LOOKUP_NETWORK) && (dev_list[i].conn_type == CONNECTION_TYPE_NETWORK)) {
				dev_network = &dev_list[i];
				break;
			}
		} else if (!strcmp(udid, dev_list[i].udid)) {
			if ((options & DEVICE_LOOKUP_USBMUX) && (dev_list[i].conn_type == CONNECTION_TYPE_USB)) {
				dev_usbmuxd = &dev_list[i];
			} else if ((options & DEVICE_LOOKUP_NETWORK) && (dev_list[i].conn_type == CONNECTION_TYPE_NETWORK)) {
				dev_network = &dev_list[i];
			}
		}
		if (dev_usbmuxd && dev_network) {
			break;
		}
	}

	if (dev_network && dev_usbmuxd) {
		dev = (options & DEVICE_LOOKUP_PREFER_NETWORK) ? dev_network : dev_usbmuxd;
	} else if (dev_network) {
		dev = dev_network;
	} else if (dev_usbmuxd) {
		dev = dev_usbmuxd;
	}

	if (dev) {
		device->handle = dev->handle;
		device->product_id = dev->product_id;
		char *t = stpncpy(device->udid, dev->udid, sizeof(device->udid)-1);
		*t = '\0';
		device->conn_type = dev->conn_type;
		memcpy(device->conn_data, dev->conn_data, sizeof(device->conn_data));
		result = 1;
	}

	free(dev_list);

	return result;
}

int usbmuxd_connect(const uint32_t handle, const unsigned short port)
{
	int sfd;
	int tag;
	int connected = 0;
	int result = EBADF;

retry:
	sfd = connect_usbmuxd_socket();
	if (sfd < 0) {
		LIBUSBMUXD_DEBUG(1, "%s: Error: Connection to usbmuxd failed: %s\n", __func__, strerror(-sfd));
		return sfd;
	}

	tag = ++use_tag;
	if (send_connect_packet(sfd, tag, handle, (uint16_t)port) <= 0) {
		LIBUSBMUXD_DEBUG(1, "%s: Error sending connect message!\n", __func__);
	} else {
		// read ACK
		uint32_t res = -1;
		LIBUSBMUXD_DEBUG(2, "%s: Reading connect result...\n", __func__);
		if (usbmuxd_get_result(sfd, tag, &res, NULL) == 1) {
			if (res == 0) {
				LIBUSBMUXD_DEBUG(2, "%s: Connect success!\n", __func__);
				connected = 1;
			} else {
				if ((res == RESULT_BADVERSION) && (proto_version == 1)) {
					proto_version = 0;
					socket_close(sfd);
					goto retry;
				}
				LIBUSBMUXD_DEBUG(1, "%s: Connect failed, Error code=%d\n", __func__, res);
				if (res == RESULT_CONNREFUSED) {
					result = ECONNREFUSED;
				} else if (res == RESULT_BADDEV) {
					result = ENODEV;
				} else {
					result = EBADF;
				}
			}
		}
	}

	if (connected) {
		return sfd;
	}

	socket_close(sfd);

	return -result;
}

int usbmuxd_disconnect(int sfd)
{
	return socket_close(sfd);
}

int usbmuxd_send(int sfd, const char *data, uint32_t len, uint32_t *sent_bytes)
{
	int num_sent;

	if (sfd < 0) {
		return -EINVAL;
	}

	num_sent = socket_send(sfd, (void*)data, len);
	if (num_sent < 0) {
		*sent_bytes = 0;
		LIBUSBMUXD_DEBUG(1, "%s: Error %d when sending: %s\n", __func__, -num_sent, strerror(-num_sent));
		return -num_sent;
	}

	if ((uint32_t)num_sent < len) {
		LIBUSBMUXD_DEBUG(1, "%s: Warning: Did not send enough (only %d of %d)\n", __func__, num_sent, len);
	}

	*sent_bytes = num_sent;

	return 0;
}

int usbmuxd_recv_timeout(int sfd, char *data, uint32_t len, uint32_t *recv_bytes, unsigned int timeout)
{
	int num_recv = socket_receive_timeout(sfd, (void*)data, len, 0, timeout);
	if (num_recv < 0) {
		*recv_bytes = 0;
		return num_recv;
	}

	*recv_bytes = num_recv;

	return 0;
}

int usbmuxd_recv(int sfd, char *data, uint32_t len, uint32_t *recv_bytes)
{
	return usbmuxd_recv_timeout(sfd, data, len, recv_bytes, 5000);
}

int usbmuxd_read_buid(char **buid)
{
	int sfd;
	int tag;
	int ret = -1;

	if (!buid) {
		return -EINVAL;
	}
	*buid = NULL;

	sfd = connect_usbmuxd_socket();
	if (sfd < 0) {
		LIBUSBMUXD_DEBUG(1, "%s: Error: Connection to usbmuxd failed: %s\n", __func__, strerror(-sfd));
		return sfd;
	}

	proto_version = 1;
	tag = ++use_tag;
	if (send_read_buid_packet(sfd, tag) <= 0) {
		LIBUSBMUXD_DEBUG(1, "%s: Error sending ReadBUID message!\n", __func__);
	} else {
		uint32_t rc = 0;
		plist_t pl = NULL;
		ret = usbmuxd_get_result(sfd, tag, &rc, &pl);
		if ((ret == 1) && (rc == 0)) {
			plist_t node = plist_dict_get_item(pl, "BUID");
			if (node && plist_get_node_type(node) == PLIST_STRING) {
				plist_get_string_val(node, buid);
			}
			ret = 0;
		} else if (ret == 1) {
			ret = -(int)rc;
		}
		plist_free(pl);
	}
	socket_close(sfd);

	return ret;
}

int usbmuxd_read_pair_record(const char* record_id, char **record_data, uint32_t *record_size)
{
	int sfd;
	int tag;
	int ret = -1;

	if (!record_id || !record_data || !record_size) {
		return -EINVAL;
	}
	*record_data = NULL;
	*record_size = 0;

	sfd = connect_usbmuxd_socket();
	if (sfd < 0) {
		LIBUSBMUXD_DEBUG(1, "%s: Error: Connection to usbmuxd failed: %s\n", __func__, strerror(-sfd));
		return sfd;
	}

	proto_version = 1;
	tag = ++use_tag;

	if (send_pair_record_packet(sfd, tag, "ReadPairRecord", record_id, 0, NULL) <= 0) {
		LIBUSBMUXD_DEBUG(1, "%s: Error sending ReadPairRecord message!\n", __func__);
	} else {
		uint32_t rc = 0;
		plist_t pl = NULL;
		ret = usbmuxd_get_result(sfd, tag, &rc, &pl);
		if ((ret == 1) && (rc == 0)) {
			plist_t node = plist_dict_get_item(pl, "PairRecordData");
			if (node && plist_get_node_type(node) == PLIST_DATA) {
				uint64_t int64val = 0;
				plist_get_data_val(node, record_data, &int64val);
				if (*record_data && int64val > 0) {
					*record_size = (uint32_t)int64val;
					ret = 0;
				}
			}
		} else if (ret == 1) {
			ret = -(int)rc;
		}
		plist_free(pl);
	}
	socket_close(sfd);

	return ret;
}

int usbmuxd_save_pair_record_with_device_id(const char* record_id, uint32_t device_id, const char *record_data, uint32_t record_size)
{
	int sfd;
	int tag;
	int ret = -1;

	if (!record_id || !record_data || !record_size) {
		return -EINVAL;
	}

	sfd = connect_usbmuxd_socket();
	if (sfd < 0) {
		LIBUSBMUXD_DEBUG(1, "%s: Error: Connection to usbmuxd failed: %s\n", __func__, strerror(-sfd));
		return sfd;
	}

	proto_version = 1;
	tag = ++use_tag;

	plist_t data = plist_new_data(record_data, record_size);
	if (send_pair_record_packet(sfd, tag, "SavePairRecord", record_id, device_id, data) <= 0) {
		LIBUSBMUXD_DEBUG(1, "%s: Error sending SavePairRecord message!\n", __func__);
	} else {
		uint32_t rc = 0;
		ret = usbmuxd_get_result(sfd, tag, &rc, NULL);
		if ((ret == 1) && (rc == 0)) {
			ret = 0;
		} else if (ret == 1) {
			ret = -(int)rc;
			LIBUSBMUXD_DEBUG(1, "%s: Error: saving pair record failed: %d\n", __func__, ret);
		}
	}
	plist_free(data);
	socket_close(sfd);

	return ret;
}

int usbmuxd_save_pair_record(const char* record_id, const char *record_data, uint32_t record_size)
{
	return usbmuxd_save_pair_record_with_device_id(record_id, 0, record_data, record_size);
}

int usbmuxd_delete_pair_record(const char* record_id)
{
	int sfd;
	int tag;
	int ret = -1;

	if (!record_id) {
		return -EINVAL;
	}

	sfd = connect_usbmuxd_socket();
	if (sfd < 0) {
		LIBUSBMUXD_DEBUG(1, "%s: Error: Connection to usbmuxd failed: %s\n", __func__, strerror(-sfd));
		return sfd;
	}

	proto_version = 1;
	tag = ++use_tag;

	if (send_pair_record_packet(sfd, tag, "DeletePairRecord", record_id, 0, NULL) <= 0) {
		LIBUSBMUXD_DEBUG(1, "%s: Error sending DeletePairRecord message!\n", __func__);
	} else {
		uint32_t rc = 0;
		ret = usbmuxd_get_result(sfd, tag, &rc, NULL);
		if ((ret == 1) && (rc == 0)) {
			ret = 0;
		} else if (ret == 1) {
			ret = -(int)rc;
			LIBUSBMUXD_DEBUG(1, "%s: Error: deleting pair record failed: %d\n", __func__, ret);
		}
	}
	socket_close(sfd);

	return ret;
}

void libusbmuxd_set_use_inotify(int set)
{
#ifdef HAVE_INOTIFY
	use_inotify = set;
#endif
}

void libusbmuxd_set_debug_level(int level)
{
	libusbmuxd_debug = level;
	socket_set_verbose(level);
}

const char* libusbmuxd_version()
{
#ifndef PACKAGE_VERSION
#error PACKAGE_VERSION is not defined
#endif
	return PACKAGE_VERSION;
}
