/*
 * libbb/libbbd.c - Bitbang library daemon
 *
 * Written 2010-2011 by Werner Almesberger
 * Copyright 2010-2011 Werner Almesberger
 *
 * 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.
 */


#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <dirent.h>
#include <poll.h>
#include <errno.h>
#include <syslog.h>
#include <sys/types.h>
#include <sys/socket.h>

#include "libbb.h"


#define	FD_DIR		"/proc/self/fd"

#define	DRIVER_NAME	"jz4740-mmc.0"
#define	UNBIND_PATH	"/sys/bus/platform/drivers/jz4740-mmc/unbind"
#define	BIND_PATH	"/sys/bus/platform/drivers/jz4740-mmc/bind"


static void closefds(void)
{
	DIR *dir;
	const struct dirent *de;
	int fd;

	dir = opendir(FD_DIR);
	if (!dir) {
		perror(FD_DIR);
		exit(1);
	}
	while ((de = readdir(dir))) {
		if (*de->d_name == '.')
			continue;
		fd = atoi(de->d_name);
		if (fd == 1 || fd == 2)
			continue;
		if (fd == dirfd(dir))
			continue;
		(void) close(fd);
	}
	closedir(dir);
}


static int echo(const char *msg, const char *path)
{
	int fd;
	ssize_t wrote;
	int len = strlen(msg);

	fd = open(path, O_WRONLY);
	if (fd < 0) {
		perror(path);
		return -errno;
	}
	wrote = write(fd, msg, len);
	if (wrote < 0) {
		perror(path);
		return -errno;
	}
	if (wrote != len) {
		fprintf(stderr, "short write: %d < %d\n", (int) wrote, len);
		return -ENOSPC;
	}
	if (close(fd) < 0) {
		perror(path);
		return -errno;
	}
	return 0;
}


static void lock_area(int fd, off_t from, off_t len)
{
	if (lseek(fd, from, SEEK_SET) == (off_t) -1) {
		perror("lseek");
		exit(1);
	}
	if (lockf(fd, F_LOCK, len) < 0) {
		perror("lockf");
		exit(1);
	}
}


static int send_fd(int s, int fd)
{
	struct msghdr msg;
	struct cmsg {
		struct cmsghdr hdr;
		int fd;
	} cmsg;

	memset(&msg, 0, sizeof(msg));
	msg.msg_control = &cmsg;
	msg.msg_controllen = sizeof(cmsg);
	cmsg.hdr.cmsg_level = SOL_SOCKET;
	cmsg.hdr.cmsg_type = SCM_RIGHTS;
	cmsg.hdr.cmsg_len = sizeof(cmsg);
	cmsg.fd = fd;
	if (sendmsg(s, &msg, 0) >= 0)
		return 0;
	perror("sendmsg");
	return -1;
}


static int wait_for_close(int s)
{
	struct pollfd fds[1];
	int res;

	fds->fd = s;
	fds->events = 0;
	fds->revents = POLLHUP;
	res = poll(fds, 1, -1);
	if (res < 0) {
		perror("poll");
		return -1;
	}
	if (!res) {
		fprintf(stderr, "poll inexplicably returned 0\n");
		return -1;
	}
	return 0;
}


int main(int argc, char **argv)
{
	int fd, res;
	int exit_code = 1;

fprintf(stderr, "setsid\n");
	if (setsid() == (pid_t) -1) {
		perror("setsid");
		exit(1);
	}
fprintf(stderr, "closefds\n");
	closefds();

fprintf(stderr, "open devmem\n");
	fd = open("/dev/mem", O_RDWR | O_SYNC);
	if (fd < 0) {
		perror("/dev/mem");
		exit(1);
	}
fprintf(stderr, "lock_area\n");
	lock_area(fd, LIBBB_GPIO_BASE+LIBBB_PORT_D_OFFSET,
	    LIBBB_PORT_D_WINDOW);

fprintf(stderr, "echo (unbind)\n");
	if (echo(DRIVER_NAME "\n", UNBIND_PATH) < 0)
		exit(1);

fprintf(stderr, "send_fd\n");
	if (send_fd(1, fd) < 0)
		goto cleanup;

fprintf(stderr, "wait_for_close\n");
	if (wait_for_close(1) < 0)
		goto cleanup;

	exit_code = 0;

cleanup:
fprintf(stderr, "echo (bind)n");
	res = echo(DRIVER_NAME "\n", BIND_PATH);
	if (res < 0) {
		/* stderr may no longer exist. report to syslog. */
		openlog("libbbd", LOG_CONS, LOG_USER);
		syslog(LOG_CRIT, "libbd failed to re-bind driver (%d)", res);
	}

fprintf(stderr, "exit\n");
	return exit_code;
}