source: protocols/purple/ft.c @ f8c9347

Last change on this file since f8c9347 was ea90275, checked in by dequis <dx@…>, at 2016-11-13T20:10:17Z

purple: fix file transfer memory management

This means cancelling transfers on logout to avoid crashes, keeping
track of timeouts, reffing and unreffing the xfers, listening to the
callbacks from UI and purple more carefully and using the correct
functions to free the correct things at the correct moments.

Originally intended to fix a crash triggered when the dcc stall timeout
kicks in after the account is offline, which is apparently very frequent
with telegram (it sends file transfers while fetching history, and
randomly disconnects a while later).

Trying to fix that meant opening a can of worms, but after three days of
work on this bug I'm pretty sure I've finished dealing with the
resulting mess and tested all the typical edge cases.

  • Property mode set to 100644
File size: 10.7 KB
Line 
1/***************************************************************************\
2*                                                                           *
3*  BitlBee - An IRC to IM gateway                                           *
4*  libpurple module - File transfer stuff                                   *
5*                                                                           *
6*  Copyright 2009-2010 Wilmer van der Gaast <wilmer@gaast.net>              *
7*                                                                           *
8*  This program is free software; you can redistribute it and/or modify     *
9*  it under the terms of the GNU General Public License as published by     *
10*  the Free Software Foundation; either version 2 of the License, or        *
11*  (at your option) any later version.                                      *
12*                                                                           *
13*  This program is distributed in the hope that it will be useful,          *
14*  but WITHOUT ANY WARRANTY; without even the implied warranty of           *
15*  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the            *
16*  GNU General Public License for more details.                             *
17*                                                                           *
18*  You should have received a copy of the GNU General Public License along  *
19*  with this program; if not, write to the Free Software Foundation, Inc.,  *
20*  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.              *
21*                                                                           *
22\***************************************************************************/
23
24/* Do file transfers via disk for now, since libpurple was really designed
25   for straight-to/from disk fts and is only just learning how to pass the
26   file contents the the UI instead (2.6.0 and higher it seems, and with
27   varying levels of success). */
28
29#include "bitlbee.h"
30#include "bpurple.h"
31
32#include <stdarg.h>
33
34#include <glib.h>
35#include <purple.h>
36
37struct prpl_xfer_data {
38        PurpleXfer *xfer;
39        file_transfer_t *ft;
40        struct im_connection *ic;
41        int fd;
42        char *fn, *handle;
43        gboolean ui_wants_data;
44        int timeout;
45};
46
47static file_transfer_t *next_ft;
48
49struct im_connection *purple_ic_by_pa(PurpleAccount *pa);
50static gboolean prplcb_xfer_new_send_cb(gpointer data, gint fd, b_input_condition cond);
51static gboolean prpl_xfer_write_request(struct file_transfer *ft);
52
53
54/* Receiving files (IM->UI): */
55static void prpl_xfer_accept(struct file_transfer *ft)
56{
57        struct prpl_xfer_data *px = ft->data;
58
59        purple_xfer_request_accepted(px->xfer, NULL);
60        prpl_xfer_write_request(ft);
61}
62
63static void prpl_xfer_canceled(struct file_transfer *ft, char *reason)
64{
65        struct prpl_xfer_data *px = ft->data;
66
67        if (px->xfer) {
68                if (!purple_xfer_is_completed(px->xfer) && !purple_xfer_is_canceled(px->xfer)) {
69                        purple_xfer_cancel_local(px->xfer);
70                }
71                px->xfer->ui_data = NULL;
72                purple_xfer_unref(px->xfer);
73                px->xfer = NULL;
74        }
75}
76
77static void prpl_xfer_free(struct file_transfer *ft)
78{
79        struct prpl_xfer_data *px = ft->data;
80        struct purple_data *pd = px->ic->proto_data;
81
82        pd->filetransfers = g_slist_remove(pd->filetransfers, px);
83
84        if (px->xfer) {
85                px->xfer->ui_data = NULL;
86                purple_xfer_unref(px->xfer);
87        }
88
89        if (px->timeout) {
90                b_event_remove(px->timeout);
91        }
92
93        g_free(px->fn);
94        g_free(px->handle);
95        if (px->fd >= 0) {
96                close(px->fd);
97        }
98        g_free(px);
99}
100
101static void prplcb_xfer_new(PurpleXfer *xfer)
102{
103        purple_xfer_ref(xfer);
104
105        if (purple_xfer_get_type(xfer) == PURPLE_XFER_RECEIVE) {
106                struct prpl_xfer_data *px = g_new0(struct prpl_xfer_data, 1);
107                struct purple_data *pd;
108
109                xfer->ui_data = px;
110                px->xfer = xfer;
111                px->fn = mktemp(g_strdup("/tmp/bitlbee-purple-ft.XXXXXX"));
112                px->fd = -1;
113                px->ic = purple_ic_by_pa(xfer->account);
114
115                pd = px->ic->proto_data;
116                pd->filetransfers = g_slist_prepend(pd->filetransfers, px);
117
118                purple_xfer_set_local_filename(xfer, px->fn);
119
120                /* Sadly the xfer struct is still empty ATM so come back after
121                   the caller is done. */
122                b_timeout_add(0, prplcb_xfer_new_send_cb, xfer);
123        } else {
124                struct file_transfer *ft = next_ft;
125                struct prpl_xfer_data *px = ft->data;
126
127                xfer->ui_data = px;
128                px->xfer = xfer;
129
130                next_ft = NULL;
131        }
132}
133
134static gboolean prplcb_xfer_new_send_cb(gpointer data, gint fd, b_input_condition cond)
135{
136        PurpleXfer *xfer = data;
137        struct im_connection *ic = purple_ic_by_pa(xfer->account);
138        struct prpl_xfer_data *px = xfer->ui_data;
139        PurpleBuddy *buddy;
140        const char *who;
141
142        buddy = purple_find_buddy(xfer->account, xfer->who);
143        who = buddy ? purple_buddy_get_name(buddy) : xfer->who;
144
145        /* TODO(wilmer): After spreading some more const goodness in BitlBee,
146           remove the evil cast below. */
147        px->ft = imcb_file_send_start(ic, (char *) who, xfer->filename, xfer->size);
148        px->ft->data = px;
149
150        px->ft->accept = prpl_xfer_accept;
151        px->ft->canceled = prpl_xfer_canceled;
152        px->ft->free = prpl_xfer_free;
153        px->ft->write_request = prpl_xfer_write_request;
154
155        return FALSE;
156}
157
158gboolean try_write_to_ui(gpointer data, gint fd, b_input_condition cond)
159{
160        struct file_transfer *ft = data;
161        struct prpl_xfer_data *px = ft->data;
162        struct stat fs;
163        off_t tx_bytes;
164
165        /* If we don't have the file opened yet, there's no data so wait. */
166        if (px->fd < 0 || !px->ui_wants_data) {
167                return FALSE;
168        }
169
170        tx_bytes = lseek(px->fd, 0, SEEK_CUR);
171        fstat(px->fd, &fs);
172
173        if (fs.st_size > tx_bytes) {
174                char buf[1024];
175                size_t n = MIN(fs.st_size - tx_bytes, sizeof(buf));
176
177                if (read(px->fd, buf, n) == n && ft->write(ft, buf, n)) {
178                        px->ui_wants_data = FALSE;
179                } else {
180                        purple_xfer_cancel_local(px->xfer);
181                        imcb_file_canceled(px->ic, ft, "Read error");
182                }
183        }
184
185        if (lseek(px->fd, 0, SEEK_CUR) == px->xfer->size) {
186                /*purple_xfer_end( px->xfer );*/
187                imcb_file_finished(px->ic, ft);
188        }
189
190        return FALSE;
191}
192
193/* UI calls this when its buffer is empty and wants more data to send to the user. */
194static gboolean prpl_xfer_write_request(struct file_transfer *ft)
195{
196        struct prpl_xfer_data *px = ft->data;
197
198        px->ui_wants_data = TRUE;
199        try_write_to_ui(ft, 0, 0);
200
201        return FALSE;
202}
203
204
205static void prplcb_xfer_destroy(PurpleXfer *xfer)
206{
207        struct prpl_xfer_data *px = xfer->ui_data;
208
209        if (px) {
210                px->xfer = NULL;
211        }
212}
213
214static void prplcb_xfer_progress(PurpleXfer *xfer, double percent)
215{
216        struct prpl_xfer_data *px = xfer->ui_data;
217
218        if (px == NULL) {
219                return;
220        }
221
222        if (purple_xfer_get_type(xfer) == PURPLE_XFER_SEND) {
223                if (*px->fn) {
224                        char *slash;
225
226                        unlink(px->fn);
227                        if ((slash = strrchr(px->fn, '/'))) {
228                                *slash = '\0';
229                                rmdir(px->fn);
230                        }
231                        *px->fn = '\0';
232                }
233
234                return;
235        }
236
237        if (px->fd == -1 && percent > 0) {
238                /* Weeeeeeeee, we're getting data! That means the file exists
239                   by now so open it and start sending to the UI. */
240                px->fd = open(px->fn, O_RDONLY);
241
242                /* Unlink it now, because we don't need it after this. */
243                unlink(px->fn);
244        }
245
246        if (percent < 1) {
247                try_write_to_ui(px->ft, 0, 0);
248        } else {
249                /* Another nice problem: If we have the whole file, it only
250                   gets closed when we return. Problem: There may still be
251                   stuff buffered and not written, we'll only see it after
252                   the caller close()s the file. So poll the file after that. */
253                b_timeout_add(0, try_write_to_ui, px->ft);
254        }
255}
256
257static void prplcb_xfer_cancel_remote(PurpleXfer *xfer)
258{
259        struct prpl_xfer_data *px = xfer->ui_data;
260
261        if (px && px->ft) {
262                imcb_file_canceled(px->ic, px->ft, "Canceled by remote end");
263        } else if (px) {
264                /* px->ft == NULL for sends, because of the two stages. :-/ */
265                imcb_error(px->ic, "File transfer cancelled by remote end");
266        }
267}
268
269
270/* Sending files (UI->IM): */
271static gboolean prpl_xfer_write(struct file_transfer *ft, char *buffer, unsigned int len);
272static gboolean purple_transfer_request_cb(gpointer data, gint fd, b_input_condition cond);
273
274void purple_transfer_request(struct im_connection *ic, file_transfer_t *ft, char *handle)
275{
276        struct prpl_xfer_data *px = g_new0(struct prpl_xfer_data, 1);
277        struct purple_data *pd;
278        char *dir, *basename;
279
280        ft->data = px;
281        px->ft = ft;
282        px->ft->free = prpl_xfer_free;
283
284        dir = g_strdup("/tmp/bitlbee-purple-ft.XXXXXX");
285        if (!mkdtemp(dir)) {
286                imcb_error(ic, "Could not create temporary file for file transfer");
287                g_free(px);
288                g_free(dir);
289                return;
290        }
291
292        if ((basename = strrchr(ft->file_name, '/'))) {
293                basename++;
294        } else {
295                basename = ft->file_name;
296        }
297        px->fn = g_strdup_printf("%s/%s", dir, basename);
298        px->fd = open(px->fn, O_WRONLY | O_CREAT, 0600);
299        g_free(dir);
300
301        if (px->fd < 0) {
302                imcb_error(ic, "Could not create temporary file for file transfer");
303                g_free(px);
304                g_free(px->fn);
305                return;
306        }
307
308        px->ic = ic;
309        px->handle = g_strdup(handle);
310
311        pd = px->ic->proto_data;
312        pd->filetransfers = g_slist_prepend(pd->filetransfers, px);
313
314        imcb_log(ic,
315                 "Due to libpurple limitations, the file has to be cached locally before proceeding with the actual file transfer. Please wait...");
316
317        px->timeout = b_timeout_add(0, purple_transfer_request_cb, ft);
318}
319
320static void purple_transfer_forward(struct file_transfer *ft)
321{
322        struct prpl_xfer_data *px = ft->data;
323        struct purple_data *pd = px->ic->proto_data;
324
325        /* xfer_new() will pick up this variable. It's a hack but we're not
326           multi-threaded anyway. */
327        next_ft = ft;
328        serv_send_file(purple_account_get_connection(pd->account),
329                   px->handle, px->fn);
330}
331
332static gboolean purple_transfer_request_cb(gpointer data, gint fd, b_input_condition cond)
333{
334        file_transfer_t *ft = data;
335        struct prpl_xfer_data *px = ft->data;
336
337        px->timeout = 0;
338
339        if (ft->write == NULL) {
340                ft->write = prpl_xfer_write;
341                imcb_file_recv_start(px->ic, ft);
342        }
343
344        ft->write_request(ft);
345
346        return FALSE;
347}
348
349static gboolean prpl_xfer_write(struct file_transfer *ft, char *buffer, unsigned int len)
350{
351        struct prpl_xfer_data *px = ft->data;
352
353        if (write(px->fd, buffer, len) != len) {
354                imcb_file_canceled(px->ic, ft, "Error while writing temporary file");
355                return FALSE;
356        }
357
358        if (lseek(px->fd, 0, SEEK_CUR) >= ft->file_size) {
359                close(px->fd);
360                px->fd = -1;
361
362                purple_transfer_forward(ft);
363                imcb_file_finished(px->ic, ft);
364                px->ft = NULL;
365        } else {
366                px->timeout = b_timeout_add(0, purple_transfer_request_cb, ft);
367        }
368
369        return TRUE;
370}
371
372void purple_transfer_cancel_all(struct im_connection *ic)
373{
374        struct purple_data *pd = ic->proto_data;
375
376        while (pd->filetransfers) {
377                struct prpl_xfer_data *px = pd->filetransfers->data;
378
379                if (px->ft) {
380                        imcb_file_canceled(ic, px->ft, "Logging out");
381                }
382
383                pd->filetransfers = g_slist_remove(pd->filetransfers, px);
384        }
385}
386
387
388
389PurpleXferUiOps bee_xfer_uiops =
390{
391        prplcb_xfer_new,           /* new_xfer */
392        prplcb_xfer_destroy,       /* destroy */
393        NULL,                      /* add_xfer */
394        prplcb_xfer_progress,      /* update_progress */
395        NULL,                      /* cancel_local */
396        prplcb_xfer_cancel_remote, /* cancel_remote */
397        NULL,                      /* ui_write */
398        NULL,                      /* ui_read */
399        NULL,                      /* data_not_sent */
400};
Note: See TracBrowser for help on using the repository browser.