Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@ var targets: [Target] = [
"unittest.cpp",
"unittest.mm"
],
sources: ["LibetpanHelperTests.swift", "unittest.swift"],
sources: ["IMAPInterruptCurrentCommandTests.swift", "LibetpanHelperTests.swift", "unittest.swift"],
resources: [
.copy("data")
]
Expand Down
12 changes: 12 additions & 0 deletions src/async/imap/MCIMAPAsyncConnection.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,18 @@ void IMAPAsyncConnection::cancelAllOperations()
mQueue->cancelAllOperations();
}

bool IMAPAsyncConnection::interruptCurrentCommand(IMAPOperation * operation)
{
// Only for the operation the queue is executing right now - its command is the one holding this
// connection. A queued operation holds nothing yet, and one that has already finished no longer
// owns the stream, so cancelling on its behalf would cut somebody else's command. The queue
// settles that question and interrupts under its own lock.
//
// Deliberately not scheduled through mQueue as an operation: the point is to unblock the
// operation the queue is running, and a queued request would wait behind that very operation.
return mQueue->interruptRunningOperation(operation);
}

void IMAPAsyncConnection::runOperation(IMAPOperation * operation)
{
if (mScheduledAutomaticDisconnect) {
Expand Down
1 change: 1 addition & 0 deletions src/async/imap/MCIMAPAsyncConnection.h
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ namespace mailcore {
virtual IMAPSession * session();

virtual void cancelAllOperations();
virtual bool interruptCurrentCommand(IMAPOperation * operation);
virtual unsigned int operationsCount();

virtual void setLastFolder(String * folder);
Expand Down
20 changes: 20 additions & 0 deletions src/async/imap/MCIMAPOperation.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,26 @@ void IMAPOperation::beforeMain()
{
}

bool IMAPOperation::interruptCurrentCommand()
{
if (mSession == NULL) {
return false;
}

return mSession->interruptCurrentCommand(this);
}

void IMAPOperation::interrupt()
{
// Called by the connection's queue while this operation is the one running, so the stream below
// is the one its command is blocked on.
if (mSession == NULL) {
return;
}

mSession->session()->interruptCurrentCommand();
}

void IMAPOperation::afterMain()
{
if (mSession->session()->isAutomaticConfigurationDone()) {
Expand Down
12 changes: 12 additions & 0 deletions src/async/imap/MCIMAPOperation.h
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,18 @@ namespace mailcore {

virtual void beforeMain();
virtual void afterMain();
virtual void interrupt();

/** Aborts this operation's IMAP command if it is the one currently running on its
connection: the blocked read returns at once instead of waiting out the socket timeout, so
the operations queued behind it (a disconnect, most importantly) run immediately.
Does nothing when the operation is not the one running.

Teardown of this connection only - it is left unusable and reconnects on next use, so call
it for a command that is being abandoned (cancelled, or given up on), never to hurry up a
command whose result still matters.
Returns whether a command was actually interrupted. */
virtual bool interruptCurrentCommand();

virtual void start();

Expand Down
2 changes: 2 additions & 0 deletions src/c/imap/CIMAPBaseOperation.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ ErrorCode CIMAPBaseOperation_error(struct CIMAPBaseOperation self) {
return static_cast<ErrorCode>(self.instance->error());
}

C_SYNTHESIZE_FUNC_WITH_SCALAR(bool, interruptCurrentCommand)

CIMAPBaseOperation CIMAPBaseOperation_setProgressBlocks(struct CIMAPBaseOperation self, CIMAPProgressBlock itemProgressBlock, CIMAPProgressBlock bodyProgressBlock, const void* userInfo) {
CIMAPBaseOperationIMAPCallback *callback = new CIMAPBaseOperationIMAPCallback(userInfo, itemProgressBlock, bodyProgressBlock);
self._callback = callback;
Expand Down
1 change: 1 addition & 0 deletions src/c/imap/CIMAPBaseOperation.h
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ extern "C" {
C_SYNTHESIZE_COBJECT_CAST_DEFINITION(CIMAPBaseOperation)

C_SYNTHESIZE_FUNC_DEFINITION(CIMAPBaseOperation, ErrorCode, error)
C_SYNTHESIZE_FUNC_DEFINITION(CIMAPBaseOperation, bool, interruptCurrentCommand)
C_SYNTHESIZE_FUNC_DEFINITION(CIMAPBaseOperation, CIMAPBaseOperation, setProgressBlocks, CIMAPProgressBlock, CIMAPProgressBlock, const void*)

CMAILCORE_EXPORT void CIMAPBaseOperation_retain(CIMAPBaseOperation operation)
Expand Down
5 changes: 5 additions & 0 deletions src/core/basetypes/MCOperation.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ void Operation::cancel()
MCB_UNLOCK(&mLock);
}

void Operation::interrupt()
{
// Nothing to interrupt by default.
}

bool Operation::isCancelled()
{
MCB_LOCK(&mLock);
Expand Down
6 changes: 6 additions & 0 deletions src/core/basetypes/MCOperation.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ namespace mailcore {
virtual OperationCallback * callback();

virtual void cancel();

/** Aborts whatever this operation is doing right now. Called by the queue, and only while
this operation is the one it is executing - so an implementation may assume it owns the
resource it is about to break. Does nothing by default. */
virtual void interrupt();

virtual bool isCancelled();

// Will be called on main thread.
Expand Down
28 changes: 28 additions & 0 deletions src/core/basetypes/MCOperationQueue.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ OperationQueue::OperationQueue()
mStopSem = mailsem_new();
mWaitingFinishedSem = mailsem_new();
mQuitting = false;
mRunningOperation = NULL;
mCallback = NULL;
#if MC_HAS_GCD
mDispatchQueue = getMainQueue();
Expand Down Expand Up @@ -63,6 +64,26 @@ void OperationQueue::addOperation(Operation * op)
startThread();
}

bool OperationQueue::interruptRunningOperation(Operation * op)
{
bool interrupted = false;

// interrupt() runs with the lock held on purpose: releasing it first would let the next
// operation start before the interruption lands, and it would be that one getting cut. The
// lock cannot keep op itself from finishing - main() runs without it - so an interrupt may
// still arrive just after the command completed; that costs a reconnect, nothing worse.
// Safe as long as interrupt() implementations stay non-blocking and never reach back into
// this queue.
MCB_LOCK(&mLock);
if ((op != NULL) && (mRunningOperation == op)) {
op->interrupt();
interrupted = true;
}
MCB_UNLOCK(&mLock);

return interrupted;
}

void OperationQueue::cancelAllOperations()
{
MCB_LOCK(&mLock);
Expand Down Expand Up @@ -122,13 +143,20 @@ void OperationQueue::runOperations()
MCAssert(op != NULL);
performOnCallbackThread(op, (Object::Method) &OperationQueue::beforeMain, op, true);

// Published only around main(): a cancelled operation whose main() is skipped never counts
// as running, so nobody can mistake an idle connection for a busy one.
if (!op->isCancelled() || op->shouldRunWhenCancelled()) {
MCB_LOCK(&mLock);
mRunningOperation = op;
MCB_UNLOCK(&mLock);

op->main();
}

op->retain()->autorelease();

MCB_LOCK(&mLock);
mRunningOperation = NULL;
mOperations->removeObjectAtIndex(0);
if (mOperations->count() == 0) {
if (mWaiting) {
Expand Down
12 changes: 12 additions & 0 deletions src/core/basetypes/MCOperationQueue.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,17 @@ namespace mailcore {

virtual void addOperation(Operation * op);
virtual void cancelAllOperations();

/** Calls interrupt() on `op` if it is the operation whose main() the queue is executing
right now. The check and the call happen under the queue's lock, so no other operation can
become the running one in between: the interrupt cannot land on whatever started next.
It does not stop `op` itself from finishing - main() runs without the lock - so an
operation that completes just as the caller reaches it gets a harmless interrupt on an
idle stream. Lets a caller abort "the command my operation is running" without the risk
of aborting somebody else's. Returns whether interrupt() was called - a caller that
measures the effect needs to tell "there was a command to break" from "there was
nothing". */
virtual bool interruptRunningOperation(Operation * op);

virtual unsigned int count();

Expand All @@ -45,6 +56,7 @@ namespace mailcore {
bool mWaiting;
struct mailsem * mWaitingFinishedSem;
bool mQuitting;
Operation * mRunningOperation;
OperationQueueCallback * mCallback;
#if MC_HAS_GCD
dispatch_queue_t mDispatchQueue;
Expand Down
13 changes: 13 additions & 0 deletions src/core/imap/MCIMAPSession.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3699,6 +3699,19 @@ void IMAPSession::disconnect()
unsetup();
}

void IMAPSession::interruptCurrentCommand()
{
// mailstream_cancel() must be called while holding the lock: unsetup() nils mImap under it and
// frees the stream right after releasing it, so a pointer grabbed and used outside the lock
// would be a use-after-free. Holding it here is safe - mailstream_cancel() only takes the
// cancel object's own mutex and writes one byte to a pipe, it never blocks.
LOCK();
if (mImap != NULL && mImap->imap_stream != NULL) {
mailstream_cancel(mImap->imap_stream);
}
UNLOCK();
}

IMAPIdentity * IMAPSession::identity(IMAPIdentity * clientIdentity, ErrorCode * pError)
{
connectIfNeeded(pError);
Expand Down
7 changes: 7 additions & 0 deletions src/core/imap/MCIMAPSession.h
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,13 @@ namespace mailcore {

virtual void connect(ErrorCode * pError);
virtual void disconnect();

/** Aborts the command currently running on this session by cancelling its stream: the
blocked read returns immediately instead of waiting out the socket timeout. Safe to call
from another thread while the session's thread is blocked in a command.
Teardown only: the cancelled state of a stream is never reset, so the session is unusable
afterwards and must be disconnected. A later connect() builds a fresh stream. */
virtual void interruptCurrentCommand();

virtual void noop(ErrorCode * pError);

Expand Down
1 change: 1 addition & 0 deletions src/include/MailCore/CIMAPBaseOperation.h
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ extern "C" {
C_SYNTHESIZE_COBJECT_CAST_DEFINITION(CIMAPBaseOperation)

C_SYNTHESIZE_FUNC_DEFINITION(CIMAPBaseOperation, ErrorCode, error)
C_SYNTHESIZE_FUNC_DEFINITION(CIMAPBaseOperation, bool, interruptCurrentCommand)
C_SYNTHESIZE_FUNC_DEFINITION(CIMAPBaseOperation, CIMAPBaseOperation, setProgressBlocks, CIMAPProgressBlock, CIMAPProgressBlock, const void*)

CMAILCORE_EXPORT void CIMAPBaseOperation_retain(CIMAPBaseOperation operation)
Expand Down
1 change: 1 addition & 0 deletions src/include/MailCore/MCIMAPAsyncConnection.h
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ namespace mailcore {
virtual IMAPSession * session();

virtual void cancelAllOperations();
virtual bool interruptCurrentCommand(IMAPOperation * operation);
virtual unsigned int operationsCount();

virtual void setLastFolder(String * folder);
Expand Down
12 changes: 12 additions & 0 deletions src/include/MailCore/MCIMAPOperation.h
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,18 @@ namespace mailcore {

virtual void beforeMain();
virtual void afterMain();
virtual void interrupt();

/** Aborts this operation's IMAP command if it is the one currently running on its
connection: the blocked read returns at once instead of waiting out the socket timeout, so
the operations queued behind it (a disconnect, most importantly) run immediately.
Does nothing when the operation is not the one running.

Teardown of this connection only - it is left unusable and reconnects on next use, so call
it for a command that is being abandoned (cancelled, or given up on), never to hurry up a
command whose result still matters.
Returns whether a command was actually interrupted. */
virtual bool interruptCurrentCommand();

virtual void start();

Expand Down
7 changes: 7 additions & 0 deletions src/include/MailCore/MCIMAPSession.h
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,13 @@ namespace mailcore {

virtual void connect(ErrorCode * pError);
virtual void disconnect();

/** Aborts the command currently running on this session by cancelling its stream: the
blocked read returns immediately instead of waiting out the socket timeout. Safe to call
from another thread while the session's thread is blocked in a command.
Teardown only: the cancelled state of a stream is never reset, so the session is unusable
afterwards and must be disconnected. A later connect() builds a fresh stream. */
virtual void interruptCurrentCommand();

virtual void noop(ErrorCode * pError);

Expand Down
6 changes: 6 additions & 0 deletions src/include/MailCore/MCOperation.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ namespace mailcore {
virtual OperationCallback * callback();

virtual void cancel();

/** Aborts whatever this operation is doing right now. Called by the queue, and only while
this operation is the one it is executing - so an implementation may assume it owns the
resource it is about to break. Does nothing by default. */
virtual void interrupt();

virtual bool isCancelled();

// Will be called on main thread.
Expand Down
12 changes: 12 additions & 0 deletions src/include/MailCore/MCOperationQueue.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,17 @@ namespace mailcore {

virtual void addOperation(Operation * op);
virtual void cancelAllOperations();

/** Calls interrupt() on `op` if it is the operation whose main() the queue is executing
right now. The check and the call happen under the queue's lock, so no other operation can
become the running one in between: the interrupt cannot land on whatever started next.
It does not stop `op` itself from finishing - main() runs without the lock - so an
operation that completes just as the caller reaches it gets a harmless interrupt on an
idle stream. Lets a caller abort "the command my operation is running" without the risk
of aborting somebody else's. Returns whether interrupt() was called - a caller that
measures the effect needs to tell "there was a command to break" from "there was
nothing". */
virtual bool interruptRunningOperation(Operation * op);

virtual unsigned int count();

Expand All @@ -45,6 +56,7 @@ namespace mailcore {
bool mWaiting;
struct mailsem * mWaitingFinishedSem;
bool mQuitting;
Operation * mRunningOperation;
OperationQueueCallback * mCallback;
#if MC_HAS_GCD
dispatch_queue_t mDispatchQueue;
Expand Down
21 changes: 21 additions & 0 deletions src/swift/imap/IMAPBaseOperation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,27 @@ public class MCOIMAPBaseOperation : MCOOperation {
internal func error() -> ErrorCode {
return baseOperation.error()
}

/**
Aborts this operation's IMAP command if it is the one currently running on its connection: the
blocked read returns at once instead of waiting out the socket timeout, so whatever is queued
behind it - a disconnect, above all - runs immediately. Does nothing when this operation is not
the one running.

Unlike cancel(), which only raises a flag mailcore checks before starting an operation, this
reaches the command already in flight. It costs the connection: the stream stays cancelled and
is rebuilt on next use, so call it for a command being abandoned, never to hurry up one whose
result still matters.

- Returns: whether a command was actually interrupted, i.e. whether this operation was the one
running. `false` means nothing was holding the connection on its behalf.
*/
@discardableResult
public func interruptCurrentCommand() -> Bool {
return mailCoreAutoreleasePool {
baseOperation.interruptCurrentCommand()
}
}

public func itemProgress(current: UInt32, maximum: UInt32) {

Expand Down
Loading
Loading