mac80211: unify SW/offload remain-on-channel

Redesign all the off-channel code, getting rid of
the generic off-channel work concept, replacing
it with a simple remain-on-channel list.

This fixes a number of small issues with the ROC
implementation:
 * offloaded remain-on-channel couldn't be queued,
   now we can queue it as well, if needed
 * in iwlwifi (the only user) offloaded ROC is
   mutually exclusive with scanning, use the new
   queue to handle that case -- I expect that it
   will later depend on a HW flag

The bigger issue though is that there's a bad bug
in the current implementation: if we get a mgmt
TX request while HW roc is active, and this new
request has a wait time, we actually schedule a
software ROC instead since we can't guarantee the
existing offloaded ROC will still be that long.
To fix this, the queuing mechanism was needed.

The queuing mechanism for offloaded ROC isn't yet
optimal, ideally we should add API to have the HW
extend the ROC if needed. We could add that later
but for now use a software implementation.

Overall, this unifies the behaviour between the
offloaded and software-implemented case as much
as possible.

Signed-off-by: Johannes Berg <johannes.berg@intel.com>
Signed-off-by: John W. Linville <linville@tuxdriver.com>
diff --git a/net/mac80211/offchannel.c b/net/mac80211/offchannel.c
index 8f482b1..abb226d 100644
--- a/net/mac80211/offchannel.c
+++ b/net/mac80211/offchannel.c
@@ -16,6 +16,7 @@
 #include <net/mac80211.h>
 #include "ieee80211_i.h"
 #include "driver-trace.h"
+#include "driver-ops.h"
 
 /*
  * Tell our hardware to disable PS.
@@ -181,32 +182,58 @@
 	mutex_unlock(&local->iflist_mtx);
 }
 
+void ieee80211_handle_roc_started(struct ieee80211_roc_work *roc)
+{
+	if (roc->notified)
+		return;
+
+	if (roc->mgmt_tx_cookie) {
+		if (!WARN_ON(!roc->frame)) {
+			ieee80211_tx_skb(roc->sdata, roc->frame);
+			roc->frame = NULL;
+		}
+	} else {
+		cfg80211_ready_on_channel(roc->sdata->dev, (unsigned long)roc,
+					  roc->chan, roc->chan_type,
+					  roc->req_duration, GFP_KERNEL);
+	}
+
+	roc->notified = true;
+}
+
 static void ieee80211_hw_roc_start(struct work_struct *work)
 {
 	struct ieee80211_local *local =
 		container_of(work, struct ieee80211_local, hw_roc_start);
-	struct ieee80211_sub_if_data *sdata;
+	struct ieee80211_roc_work *roc, *dep, *tmp;
 
 	mutex_lock(&local->mtx);
 
-	if (!local->hw_roc_channel) {
-		mutex_unlock(&local->mtx);
-		return;
-	}
+	if (list_empty(&local->roc_list))
+		goto out_unlock;
 
-	if (local->hw_roc_skb) {
-		sdata = IEEE80211_DEV_TO_SUB_IF(local->hw_roc_dev);
-		ieee80211_tx_skb(sdata, local->hw_roc_skb);
-		local->hw_roc_skb = NULL;
-	} else {
-		cfg80211_ready_on_channel(local->hw_roc_dev,
-					  local->hw_roc_cookie,
-					  local->hw_roc_channel,
-					  local->hw_roc_channel_type,
-					  local->hw_roc_duration,
-					  GFP_KERNEL);
-	}
+	roc = list_first_entry(&local->roc_list, struct ieee80211_roc_work,
+			       list);
 
+	if (!roc->started)
+		goto out_unlock;
+
+	roc->hw_begun = true;
+	roc->hw_start_time = local->hw_roc_start_time;
+
+	ieee80211_handle_roc_started(roc);
+	list_for_each_entry_safe(dep, tmp, &roc->dependents, list) {
+		ieee80211_handle_roc_started(dep);
+
+		if (dep->duration > roc->duration) {
+			u32 dur = dep->duration;
+			dep->duration = dur - roc->duration;
+			roc->duration = dur;
+			list_del(&dep->list);
+			list_add(&dep->list, &roc->list);
+		}
+	}
+ out_unlock:
 	mutex_unlock(&local->mtx);
 }
 
@@ -214,50 +241,179 @@
 {
 	struct ieee80211_local *local = hw_to_local(hw);
 
+	local->hw_roc_start_time = jiffies;
+
 	trace_api_ready_on_channel(local);
 
 	ieee80211_queue_work(hw, &local->hw_roc_start);
 }
 EXPORT_SYMBOL_GPL(ieee80211_ready_on_channel);
 
+void ieee80211_start_next_roc(struct ieee80211_local *local)
+{
+	struct ieee80211_roc_work *roc;
+
+	lockdep_assert_held(&local->mtx);
+
+	if (list_empty(&local->roc_list)) {
+		ieee80211_run_deferred_scan(local);
+		return;
+	}
+
+	roc = list_first_entry(&local->roc_list, struct ieee80211_roc_work,
+			       list);
+
+	if (local->ops->remain_on_channel) {
+		int ret, duration = roc->duration;
+
+		/* XXX: duplicated, see ieee80211_start_roc_work() */
+		if (!duration)
+			duration = 10;
+
+		ret = drv_remain_on_channel(local, roc->chan,
+					    roc->chan_type,
+					    duration);
+
+		roc->started = true;
+
+		if (ret) {
+			wiphy_warn(local->hw.wiphy,
+				   "failed to start next HW ROC (%d)\n", ret);
+			/*
+			 * queue the work struct again to avoid recursion
+			 * when multiple failures occur
+			 */
+			ieee80211_remain_on_channel_expired(&local->hw);
+		}
+	} else {
+		/* delay it a bit */
+		ieee80211_queue_delayed_work(&local->hw, &roc->work,
+					     round_jiffies_relative(HZ/2));
+	}
+}
+
+void ieee80211_roc_notify_destroy(struct ieee80211_roc_work *roc)
+{
+	struct ieee80211_roc_work *dep, *tmp;
+
+	/* was never transmitted */
+	if (roc->frame) {
+		cfg80211_mgmt_tx_status(roc->sdata->dev,
+					(unsigned long)roc->frame,
+					roc->frame->data, roc->frame->len,
+					false, GFP_KERNEL);
+		kfree_skb(roc->frame);
+	}
+
+	if (!roc->mgmt_tx_cookie)
+		cfg80211_remain_on_channel_expired(roc->sdata->dev,
+						   (unsigned long)roc,
+						   roc->chan, roc->chan_type,
+						   GFP_KERNEL);
+
+	list_for_each_entry_safe(dep, tmp, &roc->dependents, list)
+		ieee80211_roc_notify_destroy(dep);
+
+	kfree(roc);
+}
+
+void ieee80211_sw_roc_work(struct work_struct *work)
+{
+	struct ieee80211_roc_work *roc =
+		container_of(work, struct ieee80211_roc_work, work.work);
+	struct ieee80211_sub_if_data *sdata = roc->sdata;
+	struct ieee80211_local *local = sdata->local;
+
+	mutex_lock(&local->mtx);
+
+	if (roc->abort)
+		goto finish;
+
+	if (WARN_ON(list_empty(&local->roc_list)))
+		goto out_unlock;
+
+	if (WARN_ON(roc != list_first_entry(&local->roc_list,
+					    struct ieee80211_roc_work,
+					    list)))
+		goto out_unlock;
+
+	if (!roc->started) {
+		struct ieee80211_roc_work *dep;
+
+		/* start this ROC */
+
+		/* switch channel etc */
+		ieee80211_recalc_idle(local);
+
+		local->tmp_channel = roc->chan;
+		local->tmp_channel_type = roc->chan_type;
+		ieee80211_hw_config(local, 0);
+
+		/* tell userspace or send frame */
+		ieee80211_handle_roc_started(roc);
+		list_for_each_entry(dep, &roc->dependents, list)
+			ieee80211_handle_roc_started(dep);
+
+		/* if it was pure TX, just finish right away */
+		if (!roc->duration)
+			goto finish;
+
+		roc->started = true;
+		ieee80211_queue_delayed_work(&local->hw, &roc->work,
+					     msecs_to_jiffies(roc->duration));
+	} else {
+		/* finish this ROC */
+ finish:
+		list_del(&roc->list);
+		ieee80211_roc_notify_destroy(roc);
+
+		if (roc->started) {
+			drv_flush(local, false);
+
+			local->tmp_channel = NULL;
+			ieee80211_hw_config(local, 0);
+
+			ieee80211_offchannel_return(local, true);
+		}
+
+		ieee80211_recalc_idle(local);
+
+		ieee80211_start_next_roc(local);
+		ieee80211_run_deferred_scan(local);
+	}
+
+ out_unlock:
+	mutex_unlock(&local->mtx);
+}
+
 static void ieee80211_hw_roc_done(struct work_struct *work)
 {
 	struct ieee80211_local *local =
 		container_of(work, struct ieee80211_local, hw_roc_done);
+	struct ieee80211_roc_work *roc;
 
 	mutex_lock(&local->mtx);
 
-	if (!local->hw_roc_channel) {
-		mutex_unlock(&local->mtx);
-		return;
-	}
+	if (list_empty(&local->roc_list))
+		goto out_unlock;
 
-	/* was never transmitted */
-	if (local->hw_roc_skb) {
-		u64 cookie;
+	roc = list_first_entry(&local->roc_list, struct ieee80211_roc_work,
+			       list);
 
-		cookie = local->hw_roc_cookie ^ 2;
+	if (!roc->started)
+		goto out_unlock;
 
-		cfg80211_mgmt_tx_status(local->hw_roc_dev, cookie,
-					local->hw_roc_skb->data,
-					local->hw_roc_skb->len, false,
-					GFP_KERNEL);
+	list_del(&roc->list);
 
-		kfree_skb(local->hw_roc_skb);
-		local->hw_roc_skb = NULL;
-		local->hw_roc_skb_for_status = NULL;
-	}
+	ieee80211_roc_notify_destroy(roc);
 
-	if (!local->hw_roc_for_tx)
-		cfg80211_remain_on_channel_expired(local->hw_roc_dev,
-						   local->hw_roc_cookie,
-						   local->hw_roc_channel,
-						   local->hw_roc_channel_type,
-						   GFP_KERNEL);
+	/* if there's another roc, start it now */
+	ieee80211_start_next_roc(local);
 
-	local->hw_roc_channel = NULL;
-	local->hw_roc_cookie = 0;
+	/* or scan maybe */
+	ieee80211_run_deferred_scan(local);
 
+ out_unlock:
 	mutex_unlock(&local->mtx);
 }
 
@@ -271,8 +427,48 @@
 }
 EXPORT_SYMBOL_GPL(ieee80211_remain_on_channel_expired);
 
-void ieee80211_hw_roc_setup(struct ieee80211_local *local)
+void ieee80211_roc_setup(struct ieee80211_local *local)
 {
 	INIT_WORK(&local->hw_roc_start, ieee80211_hw_roc_start);
 	INIT_WORK(&local->hw_roc_done, ieee80211_hw_roc_done);
+	INIT_LIST_HEAD(&local->roc_list);
+}
+
+void ieee80211_roc_purge(struct ieee80211_sub_if_data *sdata)
+{
+	struct ieee80211_local *local = sdata->local;
+	struct ieee80211_roc_work *roc, *tmp;
+	LIST_HEAD(tmp_list);
+
+	mutex_lock(&local->mtx);
+	list_for_each_entry_safe(roc, tmp, &local->roc_list, list) {
+		if (roc->sdata != sdata)
+			continue;
+
+		if (roc->started && local->ops->remain_on_channel) {
+			/* can race, so ignore return value */
+			drv_cancel_remain_on_channel(local);
+		}
+
+		list_move_tail(&roc->list, &tmp_list);
+		roc->abort = true;
+	}
+
+	ieee80211_start_next_roc(local);
+	ieee80211_run_deferred_scan(local);
+	mutex_unlock(&local->mtx);
+
+	list_for_each_entry_safe(roc, tmp, &tmp_list, list) {
+		if (local->ops->remain_on_channel) {
+			list_del(&roc->list);
+			ieee80211_roc_notify_destroy(roc);
+		} else {
+			ieee80211_queue_delayed_work(&local->hw, &roc->work, 0);
+
+			/* work will clean up etc */
+			flush_delayed_work(&roc->work);
+		}
+	}
+
+	WARN_ON_ONCE(!list_empty(&tmp_list));
 }