/*
 * CD-ROM eject handling
 *
 * Copyright (C) 2004 TransGaming Technologies Inc.
 * 
 * Note that all of this code pretty well must execute in the real wineserver.  
 * 
 */
#include "config.h"

#include <assert.h>
#include <errno.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/poll.h>
#ifdef HAVE_SYS_TIME_H
# include <sys/time.h>
#endif
#ifdef HAVE_SYS_TYPES_H
# include <sys/types.h>
#endif
#include <unistd.h>
#include <fcntl.h>
#ifdef HAVE_SYS_IOCTL_H
# include <sys/ioctl.h>
#endif

#ifdef linux
#include <linux/cdrom.h>
#endif /* linux */

#include "config.h"
#include "wine/port.h"
#include "object.h"
#include "thread.h"
#include "server_memory.h"
#include "request.h"
#include "process.h"
#include "handle.h"
#include "mapping.h"
#include "file.h"
#include "cdrom_eject.h"


#define TIMER_INTERVAL 2000

struct count_eject_objs_params
{
    int    count;
    int    drive;
};

struct do_eject_objs_params
{
    int    count;
    int    drive;
    int    *fds;
};

struct cdrom_entry
{
    int	    drive;
#ifdef linux
    int     try_poll;
#endif /* linux */
    struct cdrom_entry *next;
    char    devicename[1];  /* variable length string */
};
START_FILE_STATIC_VARIABLES(cdrom_eject)
  struct cdrom_entry *cdrom_list;
  struct timeout_user *cdrom_eject_timeout_user;
  struct timeval cdrom_eject_request_time;
  int            cdrom_eject_request_drive;
  int            signal_cdrom_eject_request;
END_FILE_STATIC_VARIABLES(cdrom_eject)

static const char *p2p_cdrom_device_filename = "wineserver_p2p_eject_device";
static const char *client_cdrom_drive_filename = "wineserver_client_eject_drive";

#ifdef linux
/* These functions access low level linux specific CDROM ioctls */

static int wait_cmd(int fd, struct cdrom_generic_command *cgc, void *buffer,
             int len, int ddir, int timeout)
{
    struct request_sense sense;
    int ret;

    memset(&sense, 0, sizeof(sense));

    cgc->timeout = timeout;
    cgc->buffer = buffer;
    cgc->buflen = len;
    cgc->data_direction = ddir;
    cgc->sense = &sense;
    cgc->quiet = 1;

    ret = ioctl(fd, CDROM_SEND_PACKET, cgc);

    return ret;
}

int get_media_event(int fd)
{
    struct cdrom_generic_command cgc;
    unsigned char buffer[8];
    int ret;

    memset(&cgc, 0, sizeof(struct cdrom_generic_command));
    memset(buffer, 0, sizeof(buffer));

    cgc.cmd[0] = GPCMD_GET_EVENT_STATUS_NOTIFICATION;
    cgc.cmd[1] = 1;
    cgc.cmd[4] = 16;
    cgc.cmd[8] = sizeof(buffer);

    ret = wait_cmd(fd, &cgc, buffer, sizeof(buffer), CGC_DATA_READ, 600);

    if (ret < 0)
        return ret;

    return buffer[4] & 0xf;
}

#endif /* linux */

/* Write out the device name to the temp file for the client signal handler */
static void set_client_cdrom_eject_drive(int drive, int unix_tid)
{
    char *filebuf;
    size_t len = 0;
    FILE *cdrom_drive_file = NULL;
    char tidstring[255];
    
    /* Get a string representing the tid.  Note that client stores tid as DWORD,
      so we must convert it as %lu or risk having the wrong filename */
    sprintf(tidstring, "%lu", (long unsigned int)unix_tid);

    len = strlen(get_config_dir()) + strlen(client_cdrom_drive_filename) + strlen(tidstring) + 1 /* '/' */ + 1 /* '/0' */;
    filebuf = (char*)malloc(len);
    if (!filebuf)
        return;

    strcpy(filebuf, get_config_dir());
    strcat(filebuf, "/");
    strcat(filebuf, client_cdrom_drive_filename);
    strcat(filebuf, tidstring);
            
    if (debug_level)
        fprintf(stderr, "Creating client eject temp file: %s for drive: %d\n", filebuf, drive);
    
    /* Write the drive name out to the tmp file */
    cdrom_drive_file = fopen (filebuf, "w");
    free(filebuf);
    
    /* Do we have a specific drive to eject, or shall we eject them all?
     * Create 0 sized file for ejecting all drives.
     */
    if (cdrom_drive_file && drive != -1)
    {
        /* Get Drive Letter */
        char drive_letter = (char)drive;

        fwrite(&drive_letter, 1, 1, cdrom_drive_file);
    }

    if( cdrom_drive_file )
    {
        fclose(cdrom_drive_file);
    }
}

/* Check to see if the p2p temp file has a device string in it */
static int get_cdrom_eject_drive(void)
{
    char *device = NULL;
    char *filebuf;
    size_t len = 0;
    FILE *cdrom_device_file = NULL;

    len = strlen(get_config_dir()) + strlen(p2p_cdrom_device_filename) + 1 /* '/' */ + 1 /* '/0' */;
    filebuf = (char*)malloc(len);
    if (!filebuf)
        return -1;

    strcpy(filebuf, get_config_dir());
    strcat(filebuf, "/");
    strcat(filebuf, p2p_cdrom_device_filename);

    /* Read the device string out to the tmp file */
    cdrom_device_file = fopen (filebuf, "r");

    if (cdrom_device_file)
    {
        /* Calc size of device string */
        fseek(cdrom_device_file, 0, SEEK_END);
        len = ftell(cdrom_device_file);
        fseek(cdrom_device_file, 0, SEEK_SET);
        device = (char *)malloc(len+1);
        if (!device)
        {
             fclose(cdrom_device_file);
             return -1;
        }

        fread(device, 1, len, cdrom_device_file);
        device[len] = 0;

        if (debug_level)
            fprintf(stderr, "Received client eject temp file: %s\n", filebuf);
        
        fclose(cdrom_device_file);
        unlink(filebuf); /* Always clean up after ourselves */
        free(filebuf);

        if (len)
        {
            /* Search for the cdrom entry to find the drive letter */
            struct cdrom_entry *entry = SERVER_STATIC_VARIABLE(cdrom_eject, cdrom_list);

            /* Get rid of bad whitespace */
            strtok(device, "\n\t");

            while (entry)
            {
                if (strcmp(entry->devicename, device) == 0)
                {
                    free(device);
                    return entry->drive;
                }
                entry = entry->next;
            }
        }
        
        free(device);
    }
    else
    {
        if (debug_level)
            fprintf( stderr, "Unable to open %s. Perhaps it was a manual invocation?\n", filebuf );
        free(filebuf);
    }

    /* If we get here, eject all disks */
    return -1; 
}

/* Request that a process call the server back to get an fd ejection list */
static int send_process_eject_notify( struct process* process, void* data )
{
    if (process->thread_list)
    {
        /* Signal the processes to call us back to get an fd ejection list */
        set_client_cdrom_eject_drive(*(int*)data, process->thread_list->unix_tid);
        wine_tkill (process->thread_list->unix_tid, SIGUSR2);
    }
    
    return 0;
}

static void cdrom_do_umount_and_eject(char *device)
{
    int pid;

    if (!device)
        return;

    if (strlen(device) == 0)
        return;

    pid = fork();
    if (pid == -1)
        return;
    if (pid == 0)
    {
        /* Run an external program to unmount and eject the cd.  This allows that 
         * program to be setuid root if we like. */ 
        execlp("wine_forceeject", "wine_forceeject", device, NULL);
        _exit(0); /* If we get here, there was a problem, but we don't really care */
    }
    /* Don't wait for completion of the commands */
}


static void eject_cdrom(int check_drive)
{
    int drive;
    
    /* Figure out which drive to eject */
    if (check_drive)
        SERVER_STATIC_VARIABLE(cdrom_eject, cdrom_eject_request_drive) = get_cdrom_eject_drive();

    drive = SERVER_STATIC_VARIABLE(cdrom_eject, cdrom_eject_request_drive);

    /* And do it! */
    enum_processes( send_process_eject_notify, &drive );

    /* Mark the last time we had an eject request */
    gettimeofday(&SERVER_STATIC_VARIABLE(cdrom_eject, cdrom_eject_request_time), NULL);
}


/* This callback is used both for button-checking as well as for processing unmount / eject */
static void cdrom_eject_callback(void *private)
{
    struct timeval when;
    int drive = SERVER_STATIC_VARIABLE(cdrom_eject, cdrom_eject_request_drive);
    struct cdrom_entry *entry;

    /* Check to see whether we have to do an ejection, or check the button status */
    gettimeofday( &when, 0 );

    if ((SERVER_STATIC_VARIABLE(cdrom_eject, cdrom_eject_request_time).tv_sec) &&
        when.tv_sec > (SERVER_STATIC_VARIABLE(cdrom_eject, cdrom_eject_request_time).tv_sec + 1))
    {
        SERVER_STATIC_VARIABLE(cdrom_eject, cdrom_eject_request_time).tv_sec = 0;   
        
        /* Enough time has passed for all files to be closed.  Attempt to umount and eject the disk */
        entry = SERVER_STATIC_VARIABLE(cdrom_eject, cdrom_list);
        while (entry)
        {
            if ((drive == -1) || (entry->drive == drive))
                cdrom_do_umount_and_eject(entry->devicename);
            entry = entry->next;
        }
    }
#ifdef linux
    else
    {
        /* Ok, if we're here, check the actual eject buttons on the drives
         * that we can */
        int event;
        entry = SERVER_STATIC_VARIABLE(cdrom_eject, cdrom_list);
        while (entry)
        {
            if (entry->try_poll)
            {
                int fd;
    
                /* Open the cdrom fd */
                fd = open(entry->devicename, O_RDONLY | O_NONBLOCK);
                if (fd != -1)
                {
                    event = get_media_event(fd);
                    close(fd);
                
                    if (event == 1)
                    {
                        /* Eject request */
                       SERVER_STATIC_VARIABLE(cdrom_eject, cdrom_eject_request_drive) = entry->drive;
                       eject_cdrom(FALSE);
                    }
                }
            }
            entry = entry->next;
        }
    }
#endif /* linux */

    if (SERVER_STATIC_VARIABLE(cdrom_eject, signal_cdrom_eject_request))
    {
        /* Handle signaled request */
        SERVER_STATIC_VARIABLE(cdrom_eject, signal_cdrom_eject_request) = 0;
        eject_cdrom(TRUE);
    }

    /* Add the time out back again */
    add_timeout( &when, TIMER_INTERVAL );
    SERVER_STATIC_VARIABLE(cdrom_eject, cdrom_eject_timeout_user) = 
           add_timeout_user(&when, cdrom_eject_callback_tag, NULL);
    
}
timeout_callback get_cdrom_eject_callback(void) { return cdrom_eject_callback; }

/* handle an eject request at signal time */
void signal_eject_cdrom(void)
{
    if( debug_level )
        fprintf( stderr, "signal_eject_cdrom request\n" );

    /* Request handling of the signaled event at the next callback time */
    SERVER_STATIC_VARIABLE(cdrom_eject, signal_cdrom_eject_request) = 1;   
}

static void open_cdrom_entry(struct cdrom_entry *cdrom)
{
#ifdef linux
    int event;
    int fd;
    
    /* Assume failure */
    cdrom->try_poll = 0;
    
    /* Open the cdrom fd and check if it's a button-checkable cdrom; if not, close it */
    fd = open(cdrom->devicename, O_RDONLY | O_NONBLOCK);
    if (fd != -1)
    {
        event = get_media_event(fd);

        if (event >= 0)
            cdrom->try_poll = 1;
    }
    
    close(fd);
 
#endif /* linux */
}

static int count_eject_objs (struct object *obj, struct process *process, int *entryfd, void *data)
{
    struct count_eject_objs_params *params = (struct count_eject_objs_params *)data;
    struct file *file = NULL;
    struct mapping *mapping;

    if (obj->ops == file_ops_tag)
        file = (struct file *)obj;

    if (obj->ops == mapping_ops_tag)
    {
        mapping = (struct mapping *)obj;
        file = mapping->file;
    }

    if (file && file->drive == params->drive)
            params->count++;

    /* don't stop enum */
    return 0;
}

static int do_eject_objs (struct object *obj, struct process *process, int *entryfd, void *data)
{
    struct do_eject_objs_params *params = (struct do_eject_objs_params *)data;
    struct file *file = NULL;
    struct mapping *mapping = NULL;
    int fd = -1;

    if (obj->ops == file_ops_tag)
    {
        file = (struct file *)obj;
    }

    if (obj->ops == mapping_ops_tag)
    {
        mapping = (struct mapping *)obj;
        file = mapping->file;
    }

    if (file)
    {
        if (file->drive == params->drive)
        {
            /* Release the per-process context fd */
            release_context_fd(*entryfd, process, &fd);

            if (debug_level >= 2)
                fprintf( stderr, "do_eject_objs:  mapping entry fd = %d; fd = %d drive = %d\n", *entryfd, fd, file->drive );

            /* Get the fd cache entry */
            params->fds[params->count] = fd;
            params->count++;

            if (debug_level >= 2)
                fprintf( stderr, "do_eject_objs: entryfd = %d\n", *entryfd );

            /* Force immediate close the fd on the server side */
            release_context_fd(obj->fd, NULL, NULL);

            /* Now mark everything as invalid in case someone tries to get at it */
            obj->fd = INVALID_CTX_FD;
            *entryfd = INVALID_CTX_FD;
        }
    }

    /* don't stop enum */
    return 0;
}

void cdrom_eject_init(void)
{
    SERVER_STATIC_VARIABLE(cdrom_eject, cdrom_list) = NULL;
    SERVER_STATIC_VARIABLE(cdrom_eject, cdrom_eject_timeout_user) = 0;   
    SERVER_STATIC_VARIABLE(cdrom_eject, cdrom_eject_request_time).tv_sec = 0;   
    SERVER_STATIC_VARIABLE(cdrom_eject, cdrom_eject_request_drive) = 0;     
    SERVER_STATIC_VARIABLE(cdrom_eject, signal_cdrom_eject_request) = 0;     
}

void cdrom_eject_stop(void)
{
#ifdef linux
    /* Don't need to do anything here, but we might later potentially */
#endif /* linux */
}


DECL_HANDLER(get_cdrom_eject_fd_count)
{
    /* Insert request stuff here */
    int drive = req->cd_drive - 'A';

    struct count_eject_objs_params     count_params;

    count_params.drive = drive;
    count_params.count = 0;
    enum_objects (count_eject_objs, current->process, &count_params);

    reply->count = count_params.count;
}

DECL_HANDLER(get_cdrom_eject_fd_list)
{
    /* Insert request stuff here */
    unsigned int reply_fd_count = get_reply_max_size() / sizeof (int);
    int *fd_return_list = NULL;
    int drive = req->cd_drive - 'A';

    struct count_eject_objs_params     count_params;
    struct do_eject_objs_params        do_params;

    count_params.drive = drive;
    count_params.count = 0;
    enum_objects (count_eject_objs, current->process, &count_params);

    /* Check to see if we have enough room in the reply, if not error out */
    if (count_params.count > reply_fd_count)
    {
        set_error(STATUS_NO_MEMORY);
        return;
    }

    if (debug_level >= 2)
        fprintf( stderr, "get_cdrom_eject_fd_list count: %d\n", count_params.count);

    /* alloc the return list */
    fd_return_list = malloc(count_params.count * sizeof(int));
    if (!fd_return_list)
    {
        set_error(STATUS_NO_MEMORY);
        return;
    }

    /* Fill the return list */
    do_params.drive = drive;
    do_params.count = 0;
    do_params.fds = fd_return_list;
    enum_objects (do_eject_objs, current->process, &do_params);

    set_reply_data( fd_return_list, sizeof(int) * do_params.count );

    /* Free the return list */
    free (fd_return_list);
}



/* Add to the cdrom list */
DECL_HANDLER(add_cdrom_device_info)
{    
    char   *devicepath;
    size_t  devicepathlen;
    struct  cdrom_entry *entry = NULL;
    struct  timeval when;

    /* If the timeout task is already running, just return */
    if (SERVER_STATIC_VARIABLE(cdrom_eject, cdrom_eject_timeout_user))
        return;  

    devicepath = (char *)get_req_data();
    devicepathlen = get_req_data_size();

    if (devicepathlen)
    {    
        /* Check for dup entry */
        entry = SERVER_STATIC_VARIABLE(cdrom_eject, cdrom_list);
        while (entry)
        {
            if (strcmp(entry->devicename, devicepath) == 0)
                return;
            if (entry->drive == req->cd_drive)
                return;
            entry = entry->next;
        }
        
    
        /* Not a dup! - add a new entry to the list */
        entry = mem_alloc(sizeof(struct cdrom_entry) + devicepathlen);
	    
        if (!entry)
            return;
	        
        entry->drive = req->cd_drive;
        memcpy( entry->devicename, devicepath, devicepathlen );
        entry->devicename[devicepathlen] = 0;
        entry->next = SERVER_STATIC_VARIABLE(cdrom_eject, cdrom_list);
        entry->try_poll = 0;
	    
        /* Store at the list head */
        SERVER_STATIC_VARIABLE(cdrom_eject, cdrom_list) = entry;
    }
    else
    {

	/* App has requested that eject monitoring start.  
	 * Note: monitoring only works under Linux */
#ifdef linux
	/* First check each device to see if we can poll */
        entry = SERVER_STATIC_VARIABLE(cdrom_eject, cdrom_list);
        while (entry)
        {
            open_cdrom_entry(entry);
            entry = entry->next;
        }

        /* Set up a call back for polling for ejection events - every TIMER_INTERVAL milli-seconds */
        gettimeofday( &when, 0 );   
        add_timeout( &when, TIMER_INTERVAL );
    
        SERVER_STATIC_VARIABLE(cdrom_eject, cdrom_eject_timeout_user) = 
           add_timeout_user(&when, cdrom_eject_callback_tag, NULL);
#endif /* linux */

    }
}
