diff --git a/src/backend_helper.c b/src/backend_helper.c index 4256771..ecba020 100644 --- a/src/backend_helper.c +++ b/src/backend_helper.c @@ -43,6 +43,9 @@ BackendObj *get_new_BackendObj() b->num_frontends = 0; b->obj_path = NULL; b->default_printer = NULL; + g_mutex_init(&b->print_threads_mutex); + g_cond_init(&b->print_threads_cond); + b->active_print_threads = 0; return b; } @@ -1411,10 +1414,22 @@ const char *get_printer_state(PrinterCUPS *p) return str; } +void backend_obj_wait_for_print_threads(BackendObj *b) +{ + g_mutex_lock(&b->print_threads_mutex); + while (b->active_print_threads > 0) + { + logdebug("Waiting for %d active print thread(s) to finish...\n", + b->active_print_threads); + g_cond_wait(&b->print_threads_cond, &b->print_threads_mutex); + } + g_mutex_unlock(&b->print_threads_mutex); +} + void print_socket(PrinterCUPS *p, int num_settings, GVariant *settings, char *job_id_str, char *socket_path, const char *title, - char *error_msg, int error_msg_len) + char *error_msg, int error_msg_len, BackendObj *b) { ensure_dest_info(p); int num_options = 0; @@ -1511,13 +1526,26 @@ void print_socket(PrinterCUPS *p, int num_settings, GVariant *settings, thread_data->job_id = job_id; thread_data->num_options = num_options; thread_data->options = options; - thread_data->socket_fd = socket_fd; + thread_data->use_fd = 0; + thread_data->socket_fd = socket_fd; /* legacy: thread must call accept() */ snprintf(thread_data->title, sizeof(thread_data->title), "%s", title); + /* Increment active thread count and set up thread synchronization fields */ + g_mutex_lock(&b->print_threads_mutex); + b->active_print_threads++; + g_mutex_unlock(&b->print_threads_mutex); + + thread_data->print_threads_mutex = &b->print_threads_mutex; + thread_data->print_threads_cond = &b->print_threads_cond; + thread_data->active_print_threads = &b->active_print_threads; + // Create a thread for handling data transfer to CUPS pthread_t thread; if (pthread_create(&thread, NULL, print_data_thread, thread_data) != 0) { logwarn("Error creating thread"); + g_mutex_lock(&b->print_threads_mutex); + b->active_print_threads--; + g_mutex_unlock(&b->print_threads_mutex); close(socket_fd); cupsFreeOptions(num_options, options); cupsFreeDests(1, thread_data->dest); @@ -1530,6 +1558,99 @@ void print_socket(PrinterCUPS *p, int num_settings, GVariant *settings, } +void print_fd(PrinterCUPS *p, int num_settings, GVariant *settings, + char *job_id_str, int *peer_fd, const char *title, + char *error_msg, int error_msg_len, BackendObj *b) +{ + ensure_dest_info(p); + int num_options = 0; + cups_option_t *options = NULL; + error_msg[0] = '\0'; + *peer_fd = -1; + + GVariantIter *iter; + g_variant_get(settings, "a(ss)", &iter); + + char *option_name, *option_value; + for (int i = 0; i < num_settings; i++) + { + g_variant_iter_loop(iter, "(ss)", &option_name, &option_value); + logdebug(" %s : %s\n", option_name, option_value); + num_options = cupsAddOption(option_name, option_value, + num_options, &options); + } + + /* Create the CUPS job to obtain a job ID */ + int job_id = 0; + if (cupsCreateDestJob(CUPS_HTTP_DEFAULT, p->dest, p->dinfo, + &job_id, title, num_options, options) + != IPP_STATUS_OK) + { + logwarn("print_fd: job not created: %s\n", cupsLastErrorString()); + snprintf(error_msg, error_msg_len, + "job not created: %s", cupsLastErrorString()); + cupsFreeOptions(num_options, options); + return; + } + + snprintf(job_id_str, JOB_ID_BUFLEN, "%d", job_id); + + /* + * Create a connected socket pair. + * + * sv[0] — backend data thread reads print data from here. + * sv[1] — returned as *peer_fd; D-Bus handler passes this to + * the frontend via UnixFD. Frontend writes print data + * into it and closes it when done. + */ + int sv[2]; + if (socketpair(AF_UNIX, SOCK_STREAM, 0, sv) == -1) + { + logwarn("print_fd: socketpair failed: %s\n", strerror(errno)); + snprintf(error_msg, error_msg_len, + "socketpair failed: %s", strerror(errno)); + cupsFreeOptions(num_options, options); + return; + } + + PrintDataThreadData *thread_data = g_malloc(sizeof(PrintDataThreadData)); + cupsCopyDest(p->dest, 0, &thread_data->dest); + thread_data->job_id = job_id; + thread_data->num_options = num_options; + thread_data->options = options; + thread_data->socket_fd = sv[0]; /* backend reads from here */ + thread_data->use_fd = 1; /* already connected, skip accept() */ + snprintf(thread_data->title, sizeof(thread_data->title), "%s", title); + + /* Increment active thread count and set up thread synchronization fields */ + g_mutex_lock(&b->print_threads_mutex); + b->active_print_threads++; + g_mutex_unlock(&b->print_threads_mutex); + + thread_data->print_threads_mutex = &b->print_threads_mutex; + thread_data->print_threads_cond = &b->print_threads_cond; + thread_data->active_print_threads = &b->active_print_threads; + + pthread_t thread; + if (pthread_create(&thread, NULL, print_data_thread, thread_data) != 0) + { + logwarn("print_fd: pthread_create failed\n"); + g_mutex_lock(&b->print_threads_mutex); + b->active_print_threads--; + g_mutex_unlock(&b->print_threads_mutex); + close(sv[0]); + close(sv[1]); + cupsFreeOptions(num_options, options); + cupsFreeDests(1, thread_data->dest); + g_free(thread_data); + snprintf(error_msg, error_msg_len, "failed to create print thread"); + return; + } + + pthread_detach(thread); + *peer_fd = sv[1]; /* frontend writes print data into this */ +} + static void *print_data_thread(void *data) { PrintDataThreadData *thread_data = (PrintDataThreadData *)data; char *buffer = g_malloc(65536); @@ -1550,10 +1671,58 @@ static void *print_data_thread(void *data) { cupsFreeOptions(thread_data->num_options, thread_data->options); cupsFreeDests(1, thread_data->dest); g_free(buffer); + /* Save synchronization fields before freeing */ + GMutex *mtx = thread_data->print_threads_mutex; + GCond *cond = thread_data->print_threads_cond; + int *cnt = thread_data->active_print_threads; g_free(thread_data); + /* Decrement active thread count and signal waiters */ + if (cnt) + { + g_mutex_lock(mtx); + (*cnt)--; + g_cond_broadcast(cond); + g_mutex_unlock(mtx); + } return NULL; } + /* Wait for the frontend to connect and start sending print data . + * Get the connected client fd. + * FD path: socket_fd is already the connected peer , use directly. + * Legacy path: socket_fd is a listening socket , call accept(). + */ + + int client_fd; + if (thread_data->use_fd){ + client_fd = thread_data->socket_fd; + } else { + client_fd = accept(thread_data->socket_fd, NULL, NULL); + if (client_fd == -1) { + logwarn("print_data_thread: accept failed: %s\n", + strerror(errno)); + cupsFreeDestInfo(dinfo); + close(thread_data->socket_fd); + cupsFreeOptions(thread_data->num_options, thread_data->options); + cupsFreeDests(1, thread_data->dest); + g_free(buffer); + /* Save synchronization fields before freeing */ + GMutex *mtx = thread_data->print_threads_mutex; + GCond *cond = thread_data->print_threads_cond; + int *cnt = thread_data->active_print_threads; + g_free(thread_data); + /* Decrement active thread count and signal waiters */ + if (cnt) + { + g_mutex_lock(mtx); + (*cnt)--; + g_cond_broadcast(cond); + g_mutex_unlock(mtx); + } + return NULL; + } + } + /* * cupsStartDestDocument begins a chunked HTTP POST on this thread's * CUPS_HTTP_DEFAULT connection. cupsWriteRequestData and @@ -1561,6 +1730,15 @@ static void *print_data_thread(void *data) { * and finish that same HTTP POST — CUPS_HTTP_DEFAULT is per-thread * (stored in _cups_globals_t), so mixing threads here would use a * different connection and corrupt the stream. + * + * We start the document only after obtaining the client fd (after + * accept() for the legacy path, or directly for the FD path). For + * the FD path this is critical: the frontend receives sv[1] over + * D-Bus asynchronously and may not have started writing yet. Opening + * the CUPS HTTP POST before any data is available risks a CUPS + * server-side timeout ("No file in print request"). By deferring + * cupsStartDestDocument until the client fd is ready, the first + * cupsWriteRequestData call follows immediately with no gap. */ if (cupsStartDestDocument(CUPS_HTTP_DEFAULT, thread_data->dest, dinfo, @@ -1573,24 +1751,25 @@ static void *print_data_thread(void *data) { logerror("print_data_thread: could not start document: %s\n", cupsLastErrorString()); cupsFreeDestInfo(dinfo); - close(thread_data->socket_fd); - cupsFreeOptions(thread_data->num_options, thread_data->options); - cupsFreeDests(1, thread_data->dest); - g_free(buffer); - g_free(thread_data); - return NULL; - } - - /* Wait for the frontend to connect and start sending print data */ - int client_fd = accept(thread_data->socket_fd, NULL, NULL); - if (client_fd == -1) { - logwarn("print_data_thread: accept failed\n"); - cupsFreeDestInfo(dinfo); - close(thread_data->socket_fd); + close(client_fd); + if (!thread_data->use_fd) + close(thread_data->socket_fd); cupsFreeOptions(thread_data->num_options, thread_data->options); cupsFreeDests(1, thread_data->dest); g_free(buffer); + /* Save synchronization fields before freeing */ + GMutex *mtx = thread_data->print_threads_mutex; + GCond *cond = thread_data->print_threads_cond; + int *cnt = thread_data->active_print_threads; g_free(thread_data); + /* Decrement active thread count and signal waiters */ + if (cnt) + { + g_mutex_lock(mtx); + (*cnt)--; + g_cond_broadcast(cond); + g_mutex_unlock(mtx); + } return NULL; } @@ -1613,11 +1792,27 @@ static void *print_data_thread(void *data) { logerror("Document send failed: %s\n", cupsLastErrorString()); cupsFreeDestInfo(dinfo); - close(thread_data->socket_fd); + /* when using print_socket method */ + if (!thread_data->use_fd){ + close(thread_data->socket_fd); + } + cupsFreeOptions(thread_data->num_options, thread_data->options); cupsFreeDests(1, thread_data->dest); g_free(buffer); + /* Save synchronization fields before freeing */ + GMutex *mtx = thread_data->print_threads_mutex; + GCond *cond = thread_data->print_threads_cond; + int *cnt = thread_data->active_print_threads; g_free(thread_data); + /* Decrement active thread count and signal waiters */ + if (cnt) + { + g_mutex_lock(mtx); + (*cnt)--; + g_cond_broadcast(cond); + g_mutex_unlock(mtx); + } return NULL; } diff --git a/src/backend_helper.h b/src/backend_helper.h index f060041..bf94f36 100644 --- a/src/backend_helper.h +++ b/src/backend_helper.h @@ -80,6 +80,10 @@ typedef struct _BackendObj int num_frontends; char *default_printer; + + GMutex print_threads_mutex; + GCond print_threads_cond; + int active_print_threads; /* count of in-flight print threads */ } BackendObj; /** @@ -111,7 +115,18 @@ typedef struct _PrintDataThreadData { int num_options; cups_option_t *options; int socket_fd; + /* + * use_fd == 0 Legacy socket-file path: socket_fd is a listening + * socket; the thread must call accept() to get the + * connected client fd. + * use_fd == 1 FD-passing path: socket_fd is already the connected + * peer end from socketpair(); used directly, no accept(). + */ + int use_fd; char title[256]; + GMutex *print_threads_mutex; + GCond *print_threads_cond; + int *active_print_threads; } PrintDataThreadData; typedef struct _AddressList { @@ -242,7 +257,26 @@ int add_media_to_options(PrinterCUPS *p, Media *medias, int media_count, Option void print_socket(PrinterCUPS *p, int num_settings, GVariant *settings, char *job_id_str, char *socket_path, const char *title, - char *error_msg, int error_msg_len); + char *error_msg, int error_msg_len, BackendObj *b); + +/** + * FD-passing variant of print_socket(). + * + * Creates a socketpair(), starts the print-data thread on one end, and + * returns the other end in *peer_fd. The D-Bus handler passes *peer_fd + * to the frontend via D-Bus UnixFD. + * + * On failure *peer_fd is -1 and error_msg is populated. + */ +void print_fd(PrinterCUPS *p, int num_settings, GVariant *settings, + char *job_id_str, int *peer_fd, const char *title, + char *error_msg, int error_msg_len, BackendObj *b); + +/** + * Wait for all in-flight print threads to complete. + * Blocks until active_print_threads == 0. + */ +void backend_obj_wait_for_print_threads(BackendObj *b); gboolean checkRemote(const char *uri); char *extractHostFromURI(const char *uri); diff --git a/src/print_backend_cups.c b/src/print_backend_cups.c index 8a38906..e5bc595 100644 --- a/src/print_backend_cups.c +++ b/src/print_backend_cups.c @@ -2,6 +2,7 @@ #include #include #include +#include #include @@ -359,8 +360,9 @@ static gboolean on_handle_do_listing(PrintBackend *interface, remove_frontend(b, dialog_name); if (no_frontends(b)) { - // FIXME: this is racy against method calls already in-flight from dbus - g_message("No frontends connected .. exiting backend.\n"); + /* Wait for any in-flight print threads before quitting */ + backend_obj_wait_for_print_threads(b); + logdebug("No frontends connected and no print threads active — exiting.\n"); g_idle_add_once((GSourceOnceFunc)g_main_loop_quit, loop); } } @@ -524,7 +526,7 @@ static gboolean on_handle_print_socket(PrintBackend *interface, jobid[0] = '\0'; // prevent garbage being sent over D-Bus on failure socket[0] = '\0'; // used below to detect if print_socket succeeded - print_socket(p, num_settings, settings, jobid, socket, title, error_msg, sizeof(error_msg)); + print_socket(p, num_settings, settings, jobid, socket, title, error_msg, sizeof(error_msg), b); /* If socket_path is empty, print_socket failed before creating the job. * Return a D-Bus error so the frontend doesn't hang waiting for a reply. */ @@ -543,6 +545,61 @@ static gboolean on_handle_print_socket(PrintBackend *interface, return TRUE; } +static gboolean on_handle_print_fd(PrintBackend *interface, + GDBusMethodInvocation *invocation, + const gchar *printer_id, + int num_settings, + GVariant *settings, + const gchar *title, + gpointer user_data) +{ + const char *dialog_name = + g_dbus_method_invocation_get_sender(invocation); + + PrinterCUPS *p = get_printer_by_name(b, dialog_name, printer_id); + if (p == NULL) + { + g_dbus_method_invocation_return_error( + invocation, G_IO_ERROR, G_IO_ERROR_FAILED, + "Printer not found: %s", printer_id); + return TRUE; + } + + char jobid[JOB_ID_BUFLEN]; + char error_msg[256] = ""; + int peer_fd = -1; + jobid[0] = '\0'; + + print_fd(p, num_settings, settings, jobid, &peer_fd, + title, error_msg, sizeof(error_msg), b); + + if (peer_fd == -1) + { + logwarn("on_handle_print_fd: failed for printer %s: %s\n", + printer_id, + error_msg[0] ? error_msg : "unknown error"); + g_dbus_method_invocation_return_error( + invocation, G_IO_ERROR, G_IO_ERROR_FAILED, + "%s", error_msg[0] ? error_msg : "Failed to create print job"); + return TRUE; + } + + /* + * Return peer_fd to the frontend via D-Bus UnixFD. + */ + GUnixFDList *fd_list = g_unix_fd_list_new_from_array(&peer_fd, 1); + + g_dbus_method_invocation_return_value_with_unix_fd_list( + invocation, + g_variant_new("(sh)", jobid, 0), + fd_list); + + g_object_unref(fd_list); + + + return TRUE; +} + static gboolean on_handle_get_all_options(PrintBackend *interface, GDBusMethodInvocation *invocation, const gchar *printer_name, @@ -646,6 +703,10 @@ void connect_to_signals() "handle-print-socket", //signal name G_CALLBACK(on_handle_print_socket), //callback NULL); + g_signal_connect(skeleton, //instance + "handle-print-fd", //signal name + G_CALLBACK(on_handle_print_fd), //callback + NULL); g_signal_connect(skeleton, //instance "handle-get-printer-state", //signal name G_CALLBACK(on_handle_get_printer_state), //callback