| /* |
| * Copyright (c) 2017, The Linux Foundation. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions are |
| * met: |
| * * Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * * Redistributions in binary form must reproduce the above |
| * copyright notice, this list of conditions and the following |
| * disclaimer in the documentation and/or other materials provided |
| * with the distribution. |
| * * Neither the name of The Linux Foundation nor the names of its |
| * contributors may be used to endorse or promote products derived |
| * from this software without specific prior written permission. |
| * |
| * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED |
| * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF |
| * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT |
| * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS |
| * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR |
| * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
| * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR |
| * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, |
| * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE |
| * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN |
| * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| #ifndef DBG |
| #define DBG true |
| #endif /* DBG */ |
| #define LOG_TAG "IPAHALService" |
| |
| /* HIDL Includes */ |
| #include <hwbinder/IPCThreadState.h> |
| #include <hwbinder/ProcessState.h> |
| |
| /* Kernel Includes */ |
| #include <linux/netfilter/nfnetlink_compat.h> |
| |
| /* External Includes */ |
| #include <cutils/log.h> |
| #include <string> |
| #include <sys/socket.h> |
| #include <sys/types.h> |
| #include <vector> |
| |
| /* Internal Includes */ |
| #include "HAL.h" |
| #include "LocalLogBuffer.h" |
| #include "PrefixParser.h" |
| |
| /* Namespace pollution avoidance */ |
| using ::android::hardware::Void; |
| using ::android::status_t; |
| |
| using RET = ::IOffloadManager::RET; |
| using Prefix = ::IOffloadManager::Prefix; |
| |
| using ::std::map; |
| using ::std::vector; |
| |
| |
| /* ------------------------------ PUBLIC ------------------------------------ */ |
| HAL* HAL::makeIPAHAL(int version, IOffloadManager* mgr) { |
| if (DBG) |
| ALOGI("makeIPAHAL(%d, %s)", version, |
| (mgr != nullptr) ? "provided" : "null"); |
| if (nullptr == mgr) return NULL; |
| else if (version != 1) return NULL; |
| HAL* ret = new HAL(mgr); |
| if (nullptr == ret) return NULL; |
| configureRpcThreadpool(1, false); |
| ret->registerAsSystemService("ipacm"); |
| return ret; |
| } /* makeIPAHAL */ |
| |
| |
| /* ------------------------------ PRIVATE ----------------------------------- */ |
| HAL::HAL(IOffloadManager* mgr) : mLogs("HAL Function Calls", 50) { |
| mIPA = mgr; |
| mCb.clear(); |
| mCbIpa = nullptr; |
| mCbCt = nullptr; |
| } /* HAL */ |
| |
| void HAL::registerAsSystemService(const char* name) { |
| status_t ret = 0; |
| |
| ret = IOffloadControl::registerAsService(); |
| if (ret != 0) ALOGE("Failed to register IOffloadControl (%d) name(%s)", ret, name); |
| else if (DBG) { |
| ALOGI("Successfully registered IOffloadControl"); |
| } |
| |
| IOffloadConfig::registerAsService(); |
| if (ret != 0) ALOGE("Failed to register IOffloadConfig (%d)", ret); |
| else if (DBG) { |
| ALOGI("Successfully registered IOffloadConfig"); |
| } |
| } /* registerAsSystemService */ |
| |
| void HAL::doLogcatDump() { |
| ALOGD("mHandles"); |
| ALOGD("========"); |
| /* @TODO This will segfault if they aren't initialized and I don't currently |
| * care to check for initialization in a function that isn't used anyways |
| * ALOGD("fd1->%d", mHandle1->data[0]); |
| * ALOGD("fd2->%d", mHandle2->data[0]); |
| */ |
| ALOGD("========"); |
| } /* doLogcatDump */ |
| |
| HAL::BoolResult HAL::makeInputCheckFailure(string customErr) { |
| BoolResult ret; |
| ret.success = false; |
| ret.errMsg = "Failed Input Checks: " + customErr; |
| return ret; |
| } /* makeInputCheckFailure */ |
| |
| HAL::BoolResult HAL::ipaResultToBoolResult(RET in) { |
| BoolResult ret; |
| ret.success = (in >= RET::SUCCESS); |
| switch (in) { |
| case RET::FAIL_TOO_MANY_PREFIXES: |
| ret.errMsg = "Too Many Prefixes Provided"; |
| break; |
| case RET::FAIL_UNSUPPORTED: |
| ret.errMsg = "Unsupported by Hardware"; |
| break; |
| case RET::FAIL_INPUT_CHECK: |
| ret.errMsg = "Failed Input Checks"; |
| break; |
| case RET::FAIL_HARDWARE: |
| ret.errMsg = "Hardware did not accept"; |
| break; |
| case RET::FAIL_TRY_AGAIN: |
| ret.errMsg = "Try Again"; |
| break; |
| case RET::SUCCESS: |
| ret.errMsg = "Successful"; |
| break; |
| case RET::SUCCESS_DUPLICATE_CONFIG: |
| ret.errMsg = "Successful: Was a duplicate configuration"; |
| break; |
| case RET::SUCCESS_NO_OP: |
| ret.errMsg = "Successful: No action needed"; |
| break; |
| case RET::SUCCESS_OPTIMIZED: |
| ret.errMsg = "Successful: Performed optimized version of action"; |
| break; |
| default: |
| ret.errMsg = "Unknown Error"; |
| break; |
| } |
| return ret; |
| } /* ipaResultToBoolResult */ |
| |
| /* This will likely always result in doubling the number of loops the execution |
| * goes through. Obviously that is suboptimal. But if we first translate |
| * away from all HIDL specific code, then we can avoid sprinkling HIDL |
| * dependencies everywhere. |
| */ |
| vector<string> HAL::convertHidlStrToStdStr(hidl_vec<hidl_string> in) { |
| vector<string> ret; |
| for (size_t i = 0; i < in.size(); i++) { |
| string add = in[i]; |
| ret.push_back(add); |
| } |
| return ret; |
| } /* convertHidlStrToStdStr */ |
| |
| void HAL::registerEventListeners() { |
| registerIpaCb(); |
| registerCtCb(); |
| } /* registerEventListeners */ |
| |
| void HAL::registerIpaCb() { |
| if (isInitialized() && mCbIpa == nullptr) { |
| LocalLogBuffer::FunctionLog fl("registerEventListener"); |
| mCbIpa = new IpaEventRelay(mCb); |
| mIPA->registerEventListener(mCbIpa); |
| mLogs.addLog(fl); |
| } else { |
| ALOGE("Failed to registerIpaCb (isInitialized()=%s, (mCbIpa == nullptr)=%s)", |
| isInitialized() ? "true" : "false", |
| (mCbIpa == nullptr) ? "true" : "false"); |
| } |
| } /* registerIpaCb */ |
| |
| void HAL::registerCtCb() { |
| if (isInitialized() && mCbCt == nullptr) { |
| LocalLogBuffer::FunctionLog fl("registerCtTimeoutUpdater"); |
| mCbCt = new CtUpdateAmbassador(mCb); |
| mIPA->registerCtTimeoutUpdater(mCbCt); |
| mLogs.addLog(fl); |
| } else { |
| ALOGE("Failed to registerCtCb (isInitialized()=%s, (mCbCt == nullptr)=%s)", |
| isInitialized() ? "true" : "false", |
| (mCbCt == nullptr) ? "true" : "false"); |
| } |
| } /* registerCtCb */ |
| |
| void HAL::unregisterEventListeners() { |
| unregisterIpaCb(); |
| unregisterCtCb(); |
| } /* unregisterEventListeners */ |
| |
| void HAL::unregisterIpaCb() { |
| if (mCbIpa != nullptr) { |
| LocalLogBuffer::FunctionLog fl("unregisterEventListener"); |
| mIPA->unregisterEventListener(mCbIpa); |
| mCbIpa = nullptr; |
| mLogs.addLog(fl); |
| } else { |
| ALOGE("Failed to unregisterIpaCb"); |
| } |
| } /* unregisterIpaCb */ |
| |
| void HAL::unregisterCtCb() { |
| if (mCbCt != nullptr) { |
| LocalLogBuffer::FunctionLog fl("unregisterCtTimeoutUpdater"); |
| mIPA->unregisterCtTimeoutUpdater(mCbCt); |
| mCbCt = nullptr; |
| mLogs.addLog(fl); |
| } else { |
| ALOGE("Failed to unregisterCtCb"); |
| } |
| } /* unregisterCtCb */ |
| |
| void HAL::clearHandles() { |
| ALOGI("clearHandles()"); |
| /* @TODO handle this more gracefully... also remove the log |
| * |
| * Things that would be nice, but I can't do: |
| * [1] Destroy the object (it's on the stack) |
| * [2] Call freeHandle (it's private) |
| * |
| * Things I can do but are hacks: |
| * [1] Look at code and notice that setTo immediately calls freeHandle |
| */ |
| mHandle1.setTo(nullptr, true); |
| mHandle2.setTo(nullptr, true); |
| } /* clearHandles */ |
| |
| bool HAL::isInitialized() { |
| return mCb.get() != nullptr; |
| } /* isInitialized */ |
| |
| |
| /* -------------------------- IOffloadConfig -------------------------------- */ |
| Return<void> HAL::setHandles( |
| const hidl_handle &fd1, |
| const hidl_handle &fd2, |
| setHandles_cb hidl_cb |
| ) { |
| LocalLogBuffer::FunctionLog fl(__func__); |
| |
| if (fd1->numFds != 1) { |
| BoolResult res = makeInputCheckFailure("Must provide exactly one FD per handle (fd1)"); |
| hidl_cb(res.success, res.errMsg); |
| fl.setResult(res.success, res.errMsg); |
| |
| mLogs.addLog(fl); |
| return Void(); |
| } |
| |
| if (fd2->numFds != 1) { |
| BoolResult res = makeInputCheckFailure("Must provide exactly one FD per handle (fd2)"); |
| hidl_cb(res.success, res.errMsg); |
| fl.setResult(res.success, res.errMsg); |
| |
| mLogs.addLog(fl); |
| return Void(); |
| } |
| |
| /* The = operator calls freeHandle internally. Therefore, if we were using |
| * these handles previously, they're now gone... forever. But hopefully the |
| * new ones kick in very quickly. |
| * |
| * After freeing anything previously held, it will dup the FD so we have our |
| * own copy. |
| */ |
| mHandle1 = fd1; |
| mHandle2 = fd2; |
| |
| /* Log the DUPed FD instead of the actual input FD so that we can lookup |
| * this value in ls -l /proc/<pid>/<fd> |
| */ |
| fl.addArg("fd1", mHandle1->data[0]); |
| fl.addArg("fd2", mHandle2->data[0]); |
| |
| /* Try to provide each handle to IPACM. Destroy our DUPed hidl_handles if |
| * IPACM does not like either input. This keeps us from leaking FDs or |
| * providing half solutions. |
| * |
| * @TODO unfortunately, this does not cover duplicate configs where IPACM |
| * thinks it is still holding on to a handle that we would have freed above. |
| * It also probably means that IPACM would not know about the first FD being |
| * freed if it rejects the second FD. |
| */ |
| RET ipaReturn = mIPA->provideFd(mHandle1->data[0], UDP_SUBSCRIPTIONS); |
| if (ipaReturn == RET::SUCCESS) { |
| ipaReturn = mIPA->provideFd(mHandle2->data[0], TCP_SUBSCRIPTIONS); |
| } |
| |
| if (ipaReturn != RET::SUCCESS) { |
| ALOGE("IPACM failed to accept the FDs (%d %d)", mHandle1->data[0], |
| mHandle2->data[0]); |
| clearHandles(); |
| } else { |
| /* @TODO remove logs after stabilization */ |
| ALOGI("IPACM was provided two FDs (%d, %d)", mHandle1->data[0], |
| mHandle2->data[0]); |
| } |
| |
| BoolResult res = ipaResultToBoolResult(ipaReturn); |
| hidl_cb(res.success, res.errMsg); |
| |
| fl.setResult(res.success, res.errMsg); |
| mLogs.addLog(fl); |
| return Void(); |
| } /* setHandles */ |
| |
| |
| /* -------------------------- IOffloadControl ------------------------------- */ |
| Return<void> HAL::initOffload |
| ( |
| const ::android::sp<ITetheringOffloadCallback>& cb, |
| initOffload_cb hidl_cb |
| ) { |
| LocalLogBuffer::FunctionLog fl(__func__); |
| |
| if (isInitialized()) { |
| BoolResult res = makeInputCheckFailure("Already initialized"); |
| hidl_cb(res.success, res.errMsg); |
| fl.setResult(res.success, res.errMsg); |
| mLogs.addLog(fl); |
| } else { |
| /* Should storing the CB be a function? */ |
| mCb = cb; |
| registerEventListeners(); |
| BoolResult res = ipaResultToBoolResult(RET::SUCCESS); |
| hidl_cb(res.success, res.errMsg); |
| fl.setResult(res.success, res.errMsg); |
| mLogs.addLog(fl); |
| } |
| |
| return Void(); |
| } /* initOffload */ |
| |
| Return<void> HAL::stopOffload |
| ( |
| stopOffload_cb hidl_cb |
| ) { |
| LocalLogBuffer::FunctionLog fl(__func__); |
| |
| if (!isInitialized()) { |
| BoolResult res = makeInputCheckFailure("Was never initialized"); |
| hidl_cb(res.success, res.errMsg); |
| fl.setResult(res.success, res.errMsg); |
| mLogs.addLog(fl); |
| } else { |
| /* Should removing the CB be a function? */ |
| mCb.clear(); |
| unregisterEventListeners(); |
| |
| RET ipaReturn = mIPA->stopAllOffload(); |
| if (ipaReturn != RET::SUCCESS) { |
| /* Ignore IPAs return value here and provide why stopAllOffload |
| * failed. However, if IPA failed to clearAllFds, then we can't |
| * clear our map because they may still be in use. |
| */ |
| RET ret = mIPA->clearAllFds(); |
| if (ret == RET::SUCCESS) { |
| clearHandles(); |
| } |
| } else { |
| ipaReturn = mIPA->clearAllFds(); |
| /* If IPA fails, they may still be using these for some reason. */ |
| if (ipaReturn == RET::SUCCESS) { |
| clearHandles(); |
| } else { |
| ALOGE("IPACM failed to return success for clearAllFds so they will not be released..."); |
| } |
| } |
| |
| BoolResult res = ipaResultToBoolResult(ipaReturn); |
| hidl_cb(res.success, res.errMsg); |
| |
| fl.setResult(res.success, res.errMsg); |
| mLogs.addLog(fl); |
| } |
| |
| return Void(); |
| } /* stopOffload */ |
| |
| Return<void> HAL::setLocalPrefixes |
| ( |
| const hidl_vec<hidl_string>& prefixes, |
| setLocalPrefixes_cb hidl_cb |
| ) { |
| BoolResult res; |
| PrefixParser parser; |
| vector<string> prefixesStr = convertHidlStrToStdStr(prefixes); |
| |
| LocalLogBuffer::FunctionLog fl(__func__); |
| fl.addArg("prefixes", prefixesStr); |
| |
| if (!isInitialized()) { |
| BoolResult res = makeInputCheckFailure("Not initialized"); |
| } else if(prefixesStr.size() < 1) { |
| res = ipaResultToBoolResult(RET::FAIL_INPUT_CHECK); |
| } else if (!parser.add(prefixesStr)) { |
| res = makeInputCheckFailure(parser.getLastErrAsStr()); |
| } else { |
| res = ipaResultToBoolResult(RET::SUCCESS); |
| } |
| |
| hidl_cb(res.success, res.errMsg); |
| fl.setResult(res.success, res.errMsg); |
| mLogs.addLog(fl); |
| return Void(); |
| } /* setLocalPrefixes */ |
| |
| Return<void> HAL::getForwardedStats |
| ( |
| const hidl_string& upstream, |
| getForwardedStats_cb hidl_cb |
| ) { |
| LocalLogBuffer::FunctionLog fl(__func__); |
| fl.addArg("upstream", upstream); |
| |
| OffloadStatistics ret; |
| RET ipaReturn = mIPA->getStats(upstream.c_str(), true, ret); |
| if (ipaReturn == RET::SUCCESS) { |
| hidl_cb(ret.getTotalRxBytes(), ret.getTotalTxBytes()); |
| fl.setResult(ret.getTotalRxBytes(), ret.getTotalTxBytes()); |
| } else { |
| /* @TODO Ensure the output is zeroed, but this is probably not enough to |
| * tell Framework that an error has occurred. If, for example, they had |
| * not yet polled for statistics previously, they may incorrectly assume |
| * that simply no statistics have transpired on hardware path. |
| * |
| * Maybe ITetheringOffloadCallback:onEvent(OFFLOAD_STOPPED_ERROR) is |
| * enough to handle this case, time will tell. |
| */ |
| hidl_cb(0, 0); |
| fl.setResult(0, 0); |
| } |
| |
| mLogs.addLog(fl); |
| return Void(); |
| } /* getForwardedStats */ |
| |
| Return<void> HAL::setDataLimit |
| ( |
| const hidl_string& upstream, |
| uint64_t limit, |
| setDataLimit_cb hidl_cb |
| ) { |
| LocalLogBuffer::FunctionLog fl(__func__); |
| fl.addArg("upstream", upstream); |
| fl.addArg("limit", limit); |
| |
| if (!isInitialized()) { |
| BoolResult res = makeInputCheckFailure("Not initialized (setDataLimit)"); |
| hidl_cb(res.success, res.errMsg); |
| fl.setResult(res.success, res.errMsg); |
| } else { |
| RET ipaReturn = mIPA->setQuota(upstream.c_str(), limit); |
| BoolResult res = ipaResultToBoolResult(ipaReturn); |
| hidl_cb(res.success, res.errMsg); |
| fl.setResult(res.success, res.errMsg); |
| } |
| |
| mLogs.addLog(fl); |
| return Void(); |
| } /* setDataLimit */ |
| |
| Return<void> HAL::setUpstreamParameters |
| ( |
| const hidl_string& iface, |
| const hidl_string& v4Addr, |
| const hidl_string& v4Gw, |
| const hidl_vec<hidl_string>& v6Gws, |
| setUpstreamParameters_cb hidl_cb |
| ) { |
| vector<string> v6GwStrs = convertHidlStrToStdStr(v6Gws); |
| |
| LocalLogBuffer::FunctionLog fl(__func__); |
| fl.addArg("iface", iface); |
| fl.addArg("v4Addr", v4Addr); |
| fl.addArg("v4Gw", v4Gw); |
| fl.addArg("v6Gws", v6GwStrs); |
| |
| PrefixParser v4AddrParser; |
| PrefixParser v4GwParser; |
| PrefixParser v6GwParser; |
| |
| /* @TODO maybe we should enforce that these addresses and gateways are fully |
| * qualified here. But then, how do we allow them to be empty/null as well |
| * while still preserving a sane API on PrefixParser? |
| */ |
| if (!isInitialized()) { |
| BoolResult res = makeInputCheckFailure("Not initialized (setUpstreamParameters)"); |
| hidl_cb(res.success, res.errMsg); |
| fl.setResult(res.success, res.errMsg); |
| } else if (!v4AddrParser.addV4(v4Addr) && !v4Addr.empty()) { |
| BoolResult res = makeInputCheckFailure(v4AddrParser.getLastErrAsStr()); |
| hidl_cb(res.success, res.errMsg); |
| fl.setResult(res.success, res.errMsg); |
| } else if (!v4GwParser.addV4(v4Gw) && !v4Gw.empty()) { |
| BoolResult res = makeInputCheckFailure(v4GwParser.getLastErrAsStr()); |
| hidl_cb(res.success, res.errMsg); |
| fl.setResult(res.success, res.errMsg); |
| } else if (v6GwStrs.size() >= 1 && !v6GwParser.addV6(v6GwStrs)) { |
| BoolResult res = makeInputCheckFailure(v6GwParser.getLastErrAsStr()); |
| hidl_cb(res.success, res.errMsg); |
| fl.setResult(res.success, res.errMsg); |
| } else if (iface.size()>= 1) { |
| RET ipaReturn = mIPA->setUpstream( |
| iface.c_str(), |
| v4GwParser.getFirstPrefix(IP_FAM::V4), |
| v6GwParser.getFirstPrefix(IP_FAM::V6)); |
| BoolResult res = ipaResultToBoolResult(ipaReturn); |
| hidl_cb(res.success, res.errMsg); |
| fl.setResult(res.success, res.errMsg); |
| } else { |
| /* send NULL iface string when upstream down */ |
| RET ipaReturn = mIPA->setUpstream( |
| NULL, |
| v4GwParser.getFirstPrefix(IP_FAM::V4), |
| v6GwParser.getFirstPrefix(IP_FAM::V6)); |
| BoolResult res = ipaResultToBoolResult(ipaReturn); |
| hidl_cb(res.success, res.errMsg); |
| fl.setResult(res.success, res.errMsg); |
| } |
| |
| mLogs.addLog(fl); |
| return Void(); |
| } /* setUpstreamParameters */ |
| |
| Return<void> HAL::addDownstream |
| ( |
| const hidl_string& iface, |
| const hidl_string& prefix, |
| addDownstream_cb hidl_cb |
| ) { |
| LocalLogBuffer::FunctionLog fl(__func__); |
| fl.addArg("iface", iface); |
| fl.addArg("prefix", prefix); |
| |
| PrefixParser prefixParser; |
| |
| if (!isInitialized()) { |
| BoolResult res = makeInputCheckFailure("Not initialized (setUpstreamParameters)"); |
| hidl_cb(res.success, res.errMsg); |
| fl.setResult(res.success, res.errMsg); |
| } |
| else if (!prefixParser.add(prefix)) { |
| BoolResult res = makeInputCheckFailure(prefixParser.getLastErrAsStr()); |
| hidl_cb(res.success, res.errMsg); |
| fl.setResult(res.success, res.errMsg); |
| } else { |
| RET ipaReturn = mIPA->addDownstream( |
| iface.c_str(), |
| prefixParser.getFirstPrefix()); |
| BoolResult res = ipaResultToBoolResult(ipaReturn); |
| hidl_cb(res.success, res.errMsg); |
| fl.setResult(res.success, res.errMsg); |
| } |
| |
| mLogs.addLog(fl); |
| return Void(); |
| } /* addDownstream */ |
| |
| Return<void> HAL::removeDownstream |
| ( |
| const hidl_string& iface, |
| const hidl_string& prefix, |
| removeDownstream_cb hidl_cb |
| ) { |
| LocalLogBuffer::FunctionLog fl(__func__); |
| fl.addArg("iface", iface); |
| fl.addArg("prefix", prefix); |
| |
| PrefixParser prefixParser; |
| |
| if (!isInitialized()) { |
| BoolResult res = makeInputCheckFailure("Not initialized (setUpstreamParameters)"); |
| hidl_cb(res.success, res.errMsg); |
| fl.setResult(res.success, res.errMsg); |
| } |
| else if (!prefixParser.add(prefix)) { |
| BoolResult res = makeInputCheckFailure(prefixParser.getLastErrAsStr()); |
| hidl_cb(res.success, res.errMsg); |
| fl.setResult(res.success, res.errMsg); |
| } else { |
| RET ipaReturn = mIPA->removeDownstream( |
| iface.c_str(), |
| prefixParser.getFirstPrefix()); |
| BoolResult res = ipaResultToBoolResult(ipaReturn); |
| hidl_cb(res.success, res.errMsg); |
| fl.setResult(res.success, res.errMsg); |
| } |
| |
| mLogs.addLog(fl); |
| return Void(); |
| } /* removeDownstream */ |