Antoine Pitrou | 94e1696 | 2018-01-16 00:27:16 +0100 | [diff] [blame] | 1 | #include "Python.h" |
Erlend Egeberg Aasland | 01c6aa4 | 2020-11-07 20:18:37 +0100 | [diff] [blame] | 2 | #include "structmember.h" // PyMemberDef |
Victor Stinner | 4a21e57 | 2020-04-15 02:35:41 +0200 | [diff] [blame] | 3 | #include <stddef.h> // offsetof() |
Antoine Pitrou | 94e1696 | 2018-01-16 00:27:16 +0100 | [diff] [blame] | 4 | |
Erlend Egeberg Aasland | 01c6aa4 | 2020-11-07 20:18:37 +0100 | [diff] [blame] | 5 | typedef struct { |
| 6 | PyTypeObject *SimpleQueueType; |
| 7 | PyObject *EmptyError; |
| 8 | } simplequeue_state; |
Antoine Pitrou | 94e1696 | 2018-01-16 00:27:16 +0100 | [diff] [blame] | 9 | |
Erlend Egeberg Aasland | 01c6aa4 | 2020-11-07 20:18:37 +0100 | [diff] [blame] | 10 | static simplequeue_state * |
| 11 | simplequeue_get_state(PyObject *module) |
| 12 | { |
| 13 | simplequeue_state *state = PyModule_GetState(module); |
| 14 | assert(state); |
| 15 | return state; |
| 16 | } |
| 17 | static struct PyModuleDef queuemodule; |
Erlend Egeberg Aasland | bf108bb | 2020-12-28 18:47:16 +0100 | [diff] [blame] | 18 | #define simplequeue_get_state_by_type(type) \ |
Erlend Egeberg Aasland | 01c6aa4 | 2020-11-07 20:18:37 +0100 | [diff] [blame] | 19 | (simplequeue_get_state(_PyType_GetModuleByDef(type, &queuemodule))) |
Antoine Pitrou | 94e1696 | 2018-01-16 00:27:16 +0100 | [diff] [blame] | 20 | |
| 21 | typedef struct { |
| 22 | PyObject_HEAD |
| 23 | PyThread_type_lock lock; |
| 24 | int locked; |
| 25 | PyObject *lst; |
| 26 | Py_ssize_t lst_pos; |
| 27 | PyObject *weakreflist; |
| 28 | } simplequeueobject; |
| 29 | |
Erlend Egeberg Aasland | 01c6aa4 | 2020-11-07 20:18:37 +0100 | [diff] [blame] | 30 | /*[clinic input] |
| 31 | module _queue |
| 32 | class _queue.SimpleQueue "simplequeueobject *" "simplequeue_get_state_by_type(type)->SimpleQueueType" |
| 33 | [clinic start generated code]*/ |
| 34 | /*[clinic end generated code: output=da39a3ee5e6b4b0d input=0a4023fe4d198c8d]*/ |
Antoine Pitrou | 94e1696 | 2018-01-16 00:27:16 +0100 | [diff] [blame] | 35 | |
| 36 | static void |
| 37 | simplequeue_dealloc(simplequeueobject *self) |
| 38 | { |
Erlend Egeberg Aasland | 01c6aa4 | 2020-11-07 20:18:37 +0100 | [diff] [blame] | 39 | PyTypeObject *tp = Py_TYPE(self); |
| 40 | |
Victor Stinner | 1a6be91 | 2018-11-13 12:52:18 +0100 | [diff] [blame] | 41 | PyObject_GC_UnTrack(self); |
Antoine Pitrou | 94e1696 | 2018-01-16 00:27:16 +0100 | [diff] [blame] | 42 | if (self->lock != NULL) { |
| 43 | /* Unlock the lock so it's safe to free it */ |
| 44 | if (self->locked > 0) |
| 45 | PyThread_release_lock(self->lock); |
| 46 | PyThread_free_lock(self->lock); |
| 47 | } |
| 48 | Py_XDECREF(self->lst); |
| 49 | if (self->weakreflist != NULL) |
| 50 | PyObject_ClearWeakRefs((PyObject *) self); |
| 51 | Py_TYPE(self)->tp_free(self); |
Erlend Egeberg Aasland | 01c6aa4 | 2020-11-07 20:18:37 +0100 | [diff] [blame] | 52 | Py_DECREF(tp); |
Antoine Pitrou | 94e1696 | 2018-01-16 00:27:16 +0100 | [diff] [blame] | 53 | } |
| 54 | |
| 55 | static int |
| 56 | simplequeue_traverse(simplequeueobject *self, visitproc visit, void *arg) |
| 57 | { |
| 58 | Py_VISIT(self->lst); |
| 59 | return 0; |
| 60 | } |
| 61 | |
| 62 | /*[clinic input] |
| 63 | @classmethod |
| 64 | _queue.SimpleQueue.__new__ as simplequeue_new |
| 65 | |
| 66 | Simple, unbounded, reentrant FIFO queue. |
| 67 | [clinic start generated code]*/ |
| 68 | |
| 69 | static PyObject * |
| 70 | simplequeue_new_impl(PyTypeObject *type) |
| 71 | /*[clinic end generated code: output=ba97740608ba31cd input=a0674a1643e3e2fb]*/ |
| 72 | { |
| 73 | simplequeueobject *self; |
| 74 | |
| 75 | self = (simplequeueobject *) type->tp_alloc(type, 0); |
| 76 | if (self != NULL) { |
| 77 | self->weakreflist = NULL; |
| 78 | self->lst = PyList_New(0); |
| 79 | self->lock = PyThread_allocate_lock(); |
| 80 | self->lst_pos = 0; |
| 81 | if (self->lock == NULL) { |
| 82 | Py_DECREF(self); |
| 83 | PyErr_SetString(PyExc_MemoryError, "can't allocate lock"); |
| 84 | return NULL; |
| 85 | } |
| 86 | if (self->lst == NULL) { |
| 87 | Py_DECREF(self); |
| 88 | return NULL; |
| 89 | } |
| 90 | } |
| 91 | |
| 92 | return (PyObject *) self; |
| 93 | } |
| 94 | |
| 95 | /*[clinic input] |
| 96 | _queue.SimpleQueue.put |
| 97 | item: object |
| 98 | block: bool = True |
| 99 | timeout: object = None |
| 100 | |
| 101 | Put the item on the queue. |
| 102 | |
| 103 | The optional 'block' and 'timeout' arguments are ignored, as this method |
| 104 | never blocks. They are provided for compatibility with the Queue class. |
| 105 | |
| 106 | [clinic start generated code]*/ |
| 107 | |
| 108 | static PyObject * |
| 109 | _queue_SimpleQueue_put_impl(simplequeueobject *self, PyObject *item, |
| 110 | int block, PyObject *timeout) |
| 111 | /*[clinic end generated code: output=4333136e88f90d8b input=6e601fa707a782d5]*/ |
| 112 | { |
| 113 | /* BEGIN GIL-protected critical section */ |
| 114 | if (PyList_Append(self->lst, item) < 0) |
| 115 | return NULL; |
| 116 | if (self->locked) { |
| 117 | /* A get() may be waiting, wake it up */ |
| 118 | self->locked = 0; |
| 119 | PyThread_release_lock(self->lock); |
| 120 | } |
| 121 | /* END GIL-protected critical section */ |
| 122 | Py_RETURN_NONE; |
| 123 | } |
| 124 | |
| 125 | /*[clinic input] |
| 126 | _queue.SimpleQueue.put_nowait |
| 127 | item: object |
| 128 | |
| 129 | Put an item into the queue without blocking. |
| 130 | |
| 131 | This is exactly equivalent to `put(item)` and is only provided |
| 132 | for compatibility with the Queue class. |
| 133 | |
| 134 | [clinic start generated code]*/ |
| 135 | |
| 136 | static PyObject * |
| 137 | _queue_SimpleQueue_put_nowait_impl(simplequeueobject *self, PyObject *item) |
| 138 | /*[clinic end generated code: output=0990536715efb1f1 input=36b1ea96756b2ece]*/ |
| 139 | { |
| 140 | return _queue_SimpleQueue_put_impl(self, item, 0, Py_None); |
| 141 | } |
| 142 | |
| 143 | static PyObject * |
| 144 | simplequeue_pop_item(simplequeueobject *self) |
| 145 | { |
| 146 | Py_ssize_t count, n; |
| 147 | PyObject *item; |
| 148 | |
| 149 | n = PyList_GET_SIZE(self->lst); |
| 150 | assert(self->lst_pos < n); |
| 151 | |
| 152 | item = PyList_GET_ITEM(self->lst, self->lst_pos); |
| 153 | Py_INCREF(Py_None); |
| 154 | PyList_SET_ITEM(self->lst, self->lst_pos, Py_None); |
| 155 | self->lst_pos += 1; |
| 156 | count = n - self->lst_pos; |
| 157 | if (self->lst_pos > count) { |
| 158 | /* The list is more than 50% empty, reclaim space at the beginning */ |
| 159 | if (PyList_SetSlice(self->lst, 0, self->lst_pos, NULL)) { |
| 160 | /* Undo pop */ |
| 161 | self->lst_pos -= 1; |
| 162 | PyList_SET_ITEM(self->lst, self->lst_pos, item); |
| 163 | return NULL; |
| 164 | } |
| 165 | self->lst_pos = 0; |
| 166 | } |
| 167 | return item; |
| 168 | } |
| 169 | |
| 170 | /*[clinic input] |
| 171 | _queue.SimpleQueue.get |
Erlend Egeberg Aasland | 01c6aa4 | 2020-11-07 20:18:37 +0100 | [diff] [blame] | 172 | |
| 173 | cls: defining_class |
| 174 | / |
Antoine Pitrou | 94e1696 | 2018-01-16 00:27:16 +0100 | [diff] [blame] | 175 | block: bool = True |
| 176 | timeout: object = None |
| 177 | |
| 178 | Remove and return an item from the queue. |
| 179 | |
| 180 | If optional args 'block' is true and 'timeout' is None (the default), |
| 181 | block if necessary until an item is available. If 'timeout' is |
| 182 | a non-negative number, it blocks at most 'timeout' seconds and raises |
| 183 | the Empty exception if no item was available within that time. |
| 184 | Otherwise ('block' is false), return an item if one is immediately |
| 185 | available, else raise the Empty exception ('timeout' is ignored |
| 186 | in that case). |
| 187 | |
| 188 | [clinic start generated code]*/ |
| 189 | |
| 190 | static PyObject * |
Erlend Egeberg Aasland | 01c6aa4 | 2020-11-07 20:18:37 +0100 | [diff] [blame] | 191 | _queue_SimpleQueue_get_impl(simplequeueobject *self, PyTypeObject *cls, |
| 192 | int block, PyObject *timeout) |
| 193 | /*[clinic end generated code: output=1969aefa7db63666 input=5fc4d56b9a54757e]*/ |
Antoine Pitrou | 94e1696 | 2018-01-16 00:27:16 +0100 | [diff] [blame] | 194 | { |
| 195 | _PyTime_t endtime = 0; |
| 196 | _PyTime_t timeout_val; |
| 197 | PyObject *item; |
| 198 | PyLockStatus r; |
| 199 | PY_TIMEOUT_T microseconds; |
| 200 | |
| 201 | if (block == 0) { |
| 202 | /* Non-blocking */ |
| 203 | microseconds = 0; |
| 204 | } |
| 205 | else if (timeout != Py_None) { |
| 206 | /* With timeout */ |
| 207 | if (_PyTime_FromSecondsObject(&timeout_val, |
| 208 | timeout, _PyTime_ROUND_CEILING) < 0) |
| 209 | return NULL; |
| 210 | if (timeout_val < 0) { |
| 211 | PyErr_SetString(PyExc_ValueError, |
| 212 | "'timeout' must be a non-negative number"); |
| 213 | return NULL; |
| 214 | } |
| 215 | microseconds = _PyTime_AsMicroseconds(timeout_val, |
| 216 | _PyTime_ROUND_CEILING); |
| 217 | if (microseconds >= PY_TIMEOUT_MAX) { |
| 218 | PyErr_SetString(PyExc_OverflowError, |
| 219 | "timeout value is too large"); |
| 220 | return NULL; |
| 221 | } |
| 222 | endtime = _PyTime_GetMonotonicClock() + timeout_val; |
| 223 | } |
| 224 | else { |
| 225 | /* Infinitely blocking */ |
| 226 | microseconds = -1; |
| 227 | } |
| 228 | |
| 229 | /* put() signals the queue to be non-empty by releasing the lock. |
| 230 | * So we simply try to acquire the lock in a loop, until the condition |
| 231 | * (queue non-empty) becomes true. |
| 232 | */ |
| 233 | while (self->lst_pos == PyList_GET_SIZE(self->lst)) { |
| 234 | /* First a simple non-blocking try without releasing the GIL */ |
| 235 | r = PyThread_acquire_lock_timed(self->lock, 0, 0); |
| 236 | if (r == PY_LOCK_FAILURE && microseconds != 0) { |
| 237 | Py_BEGIN_ALLOW_THREADS |
| 238 | r = PyThread_acquire_lock_timed(self->lock, microseconds, 1); |
| 239 | Py_END_ALLOW_THREADS |
| 240 | } |
| 241 | if (r == PY_LOCK_INTR && Py_MakePendingCalls() < 0) { |
| 242 | return NULL; |
| 243 | } |
| 244 | if (r == PY_LOCK_FAILURE) { |
Erlend Egeberg Aasland | 01c6aa4 | 2020-11-07 20:18:37 +0100 | [diff] [blame] | 245 | PyObject *module = PyType_GetModule(cls); |
| 246 | simplequeue_state *state = simplequeue_get_state(module); |
Antoine Pitrou | 94e1696 | 2018-01-16 00:27:16 +0100 | [diff] [blame] | 247 | /* Timed out */ |
Erlend Egeberg Aasland | 01c6aa4 | 2020-11-07 20:18:37 +0100 | [diff] [blame] | 248 | PyErr_SetNone(state->EmptyError); |
Antoine Pitrou | 94e1696 | 2018-01-16 00:27:16 +0100 | [diff] [blame] | 249 | return NULL; |
| 250 | } |
| 251 | self->locked = 1; |
| 252 | /* Adjust timeout for next iteration (if any) */ |
| 253 | if (endtime > 0) { |
| 254 | timeout_val = endtime - _PyTime_GetMonotonicClock(); |
| 255 | microseconds = _PyTime_AsMicroseconds(timeout_val, _PyTime_ROUND_CEILING); |
| 256 | } |
| 257 | } |
| 258 | /* BEGIN GIL-protected critical section */ |
| 259 | assert(self->lst_pos < PyList_GET_SIZE(self->lst)); |
| 260 | item = simplequeue_pop_item(self); |
| 261 | if (self->locked) { |
| 262 | PyThread_release_lock(self->lock); |
| 263 | self->locked = 0; |
| 264 | } |
| 265 | /* END GIL-protected critical section */ |
| 266 | |
| 267 | return item; |
| 268 | } |
| 269 | |
| 270 | /*[clinic input] |
| 271 | _queue.SimpleQueue.get_nowait |
| 272 | |
Erlend Egeberg Aasland | 01c6aa4 | 2020-11-07 20:18:37 +0100 | [diff] [blame] | 273 | cls: defining_class |
| 274 | / |
| 275 | |
Antoine Pitrou | 94e1696 | 2018-01-16 00:27:16 +0100 | [diff] [blame] | 276 | Remove and return an item from the queue without blocking. |
| 277 | |
| 278 | Only get an item if one is immediately available. Otherwise |
| 279 | raise the Empty exception. |
| 280 | [clinic start generated code]*/ |
| 281 | |
| 282 | static PyObject * |
Erlend Egeberg Aasland | 01c6aa4 | 2020-11-07 20:18:37 +0100 | [diff] [blame] | 283 | _queue_SimpleQueue_get_nowait_impl(simplequeueobject *self, |
| 284 | PyTypeObject *cls) |
| 285 | /*[clinic end generated code: output=620c58e2750f8b8a input=842f732bf04216d3]*/ |
Antoine Pitrou | 94e1696 | 2018-01-16 00:27:16 +0100 | [diff] [blame] | 286 | { |
Erlend Egeberg Aasland | 01c6aa4 | 2020-11-07 20:18:37 +0100 | [diff] [blame] | 287 | return _queue_SimpleQueue_get_impl(self, cls, 0, Py_None); |
Antoine Pitrou | 94e1696 | 2018-01-16 00:27:16 +0100 | [diff] [blame] | 288 | } |
| 289 | |
| 290 | /*[clinic input] |
| 291 | _queue.SimpleQueue.empty -> bool |
| 292 | |
| 293 | Return True if the queue is empty, False otherwise (not reliable!). |
| 294 | [clinic start generated code]*/ |
| 295 | |
| 296 | static int |
| 297 | _queue_SimpleQueue_empty_impl(simplequeueobject *self) |
| 298 | /*[clinic end generated code: output=1a02a1b87c0ef838 input=1a98431c45fd66f9]*/ |
| 299 | { |
| 300 | return self->lst_pos == PyList_GET_SIZE(self->lst); |
| 301 | } |
| 302 | |
| 303 | /*[clinic input] |
| 304 | _queue.SimpleQueue.qsize -> Py_ssize_t |
| 305 | |
| 306 | Return the approximate size of the queue (not reliable!). |
| 307 | [clinic start generated code]*/ |
| 308 | |
| 309 | static Py_ssize_t |
| 310 | _queue_SimpleQueue_qsize_impl(simplequeueobject *self) |
| 311 | /*[clinic end generated code: output=f9dcd9d0a90e121e input=7a74852b407868a1]*/ |
| 312 | { |
| 313 | return PyList_GET_SIZE(self->lst) - self->lst_pos; |
| 314 | } |
| 315 | |
Erlend Egeberg Aasland | 01c6aa4 | 2020-11-07 20:18:37 +0100 | [diff] [blame] | 316 | static int |
| 317 | queue_traverse(PyObject *m, visitproc visit, void *arg) |
| 318 | { |
| 319 | simplequeue_state *state = simplequeue_get_state(m); |
| 320 | Py_VISIT(state->SimpleQueueType); |
| 321 | Py_VISIT(state->EmptyError); |
| 322 | return 0; |
| 323 | } |
| 324 | |
| 325 | static int |
| 326 | queue_clear(PyObject *m) |
| 327 | { |
| 328 | simplequeue_state *state = simplequeue_get_state(m); |
| 329 | Py_CLEAR(state->SimpleQueueType); |
| 330 | Py_CLEAR(state->EmptyError); |
| 331 | return 0; |
| 332 | } |
| 333 | |
| 334 | static void |
| 335 | queue_free(void *m) |
| 336 | { |
| 337 | queue_clear((PyObject *)m); |
| 338 | } |
Antoine Pitrou | 94e1696 | 2018-01-16 00:27:16 +0100 | [diff] [blame] | 339 | |
| 340 | #include "clinic/_queuemodule.c.h" |
| 341 | |
| 342 | |
| 343 | static PyMethodDef simplequeue_methods[] = { |
| 344 | _QUEUE_SIMPLEQUEUE_EMPTY_METHODDEF |
| 345 | _QUEUE_SIMPLEQUEUE_GET_METHODDEF |
| 346 | _QUEUE_SIMPLEQUEUE_GET_NOWAIT_METHODDEF |
| 347 | _QUEUE_SIMPLEQUEUE_PUT_METHODDEF |
| 348 | _QUEUE_SIMPLEQUEUE_PUT_NOWAIT_METHODDEF |
| 349 | _QUEUE_SIMPLEQUEUE_QSIZE_METHODDEF |
Batuhan Taşkaya | 0361556 | 2020-04-10 17:46:36 +0300 | [diff] [blame] | 350 | {"__class_getitem__", (PyCFunction)Py_GenericAlias, |
| 351 | METH_O|METH_CLASS, PyDoc_STR("See PEP 585")}, |
Antoine Pitrou | 94e1696 | 2018-01-16 00:27:16 +0100 | [diff] [blame] | 352 | {NULL, NULL} /* sentinel */ |
| 353 | }; |
| 354 | |
Erlend Egeberg Aasland | 01c6aa4 | 2020-11-07 20:18:37 +0100 | [diff] [blame] | 355 | static struct PyMemberDef simplequeue_members[] = { |
| 356 | {"__weaklistoffset__", T_PYSSIZET, offsetof(simplequeueobject, weakreflist), READONLY}, |
| 357 | {NULL}, |
| 358 | }; |
Antoine Pitrou | 94e1696 | 2018-01-16 00:27:16 +0100 | [diff] [blame] | 359 | |
Erlend Egeberg Aasland | 01c6aa4 | 2020-11-07 20:18:37 +0100 | [diff] [blame] | 360 | static PyType_Slot simplequeue_slots[] = { |
| 361 | {Py_tp_dealloc, simplequeue_dealloc}, |
| 362 | {Py_tp_doc, (void *)simplequeue_new__doc__}, |
| 363 | {Py_tp_traverse, simplequeue_traverse}, |
| 364 | {Py_tp_members, simplequeue_members}, |
| 365 | {Py_tp_methods, simplequeue_methods}, |
| 366 | {Py_tp_new, simplequeue_new}, |
| 367 | {0, NULL}, |
| 368 | }; |
| 369 | |
| 370 | static PyType_Spec simplequeue_spec = { |
| 371 | .name = "_queue.SimpleQueue", |
| 372 | .basicsize = sizeof(simplequeueobject), |
| 373 | .flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC, |
| 374 | .slots = simplequeue_slots, |
Antoine Pitrou | 94e1696 | 2018-01-16 00:27:16 +0100 | [diff] [blame] | 375 | }; |
| 376 | |
| 377 | |
| 378 | /* Initialization function */ |
| 379 | |
| 380 | PyDoc_STRVAR(queue_module_doc, |
| 381 | "C implementation of the Python queue module.\n\ |
| 382 | This module is an implementation detail, please do not use it directly."); |
| 383 | |
Christian Heimes | 3094dd5 | 2020-11-19 09:24:37 +0100 | [diff] [blame] | 384 | static int |
| 385 | queuemodule_exec(PyObject *module) |
| 386 | { |
| 387 | simplequeue_state *state = simplequeue_get_state(module); |
| 388 | |
| 389 | state->EmptyError = PyErr_NewExceptionWithDoc( |
| 390 | "_queue.Empty", |
| 391 | "Exception raised by Queue.get(block=0)/get_nowait().", |
| 392 | NULL, NULL); |
| 393 | if (state->EmptyError == NULL) { |
| 394 | return -1; |
| 395 | } |
| 396 | if (PyModule_AddObjectRef(module, "Empty", state->EmptyError) < 0) { |
| 397 | return -1; |
| 398 | } |
| 399 | |
| 400 | state->SimpleQueueType = (PyTypeObject *)PyType_FromModuleAndSpec( |
| 401 | module, &simplequeue_spec, NULL); |
| 402 | if (state->SimpleQueueType == NULL) { |
| 403 | return -1; |
| 404 | } |
| 405 | if (PyModule_AddType(module, state->SimpleQueueType) < 0) { |
| 406 | return -1; |
| 407 | } |
| 408 | |
| 409 | return 0; |
| 410 | } |
| 411 | |
| 412 | static PyModuleDef_Slot queuemodule_slots[] = { |
| 413 | {Py_mod_exec, queuemodule_exec}, |
| 414 | {0, NULL} |
| 415 | }; |
| 416 | |
| 417 | |
Antoine Pitrou | 94e1696 | 2018-01-16 00:27:16 +0100 | [diff] [blame] | 418 | static struct PyModuleDef queuemodule = { |
Erlend Egeberg Aasland | 01c6aa4 | 2020-11-07 20:18:37 +0100 | [diff] [blame] | 419 | .m_base = PyModuleDef_HEAD_INIT, |
| 420 | .m_name = "_queue", |
| 421 | .m_doc = queue_module_doc, |
| 422 | .m_size = sizeof(simplequeue_state), |
Christian Heimes | 3094dd5 | 2020-11-19 09:24:37 +0100 | [diff] [blame] | 423 | .m_slots = queuemodule_slots, |
Erlend Egeberg Aasland | 01c6aa4 | 2020-11-07 20:18:37 +0100 | [diff] [blame] | 424 | .m_traverse = queue_traverse, |
| 425 | .m_clear = queue_clear, |
| 426 | .m_free = queue_free, |
Antoine Pitrou | 94e1696 | 2018-01-16 00:27:16 +0100 | [diff] [blame] | 427 | }; |
| 428 | |
| 429 | |
| 430 | PyMODINIT_FUNC |
| 431 | PyInit__queue(void) |
| 432 | { |
Christian Heimes | 3094dd5 | 2020-11-19 09:24:37 +0100 | [diff] [blame] | 433 | return PyModuleDef_Init(&queuemodule); |
Antoine Pitrou | 94e1696 | 2018-01-16 00:27:16 +0100 | [diff] [blame] | 434 | } |