LogReader.cpp revision 501c373916e292764400dbae735f44b33378400f
10175b0747a1f55329109e84c9a1322dcb95e2848Mark Salyzyn/* 20175b0747a1f55329109e84c9a1322dcb95e2848Mark Salyzyn * Copyright (C) 2012-2013 The Android Open Source Project 30175b0747a1f55329109e84c9a1322dcb95e2848Mark Salyzyn * 40175b0747a1f55329109e84c9a1322dcb95e2848Mark Salyzyn * Licensed under the Apache License, Version 2.0 (the "License"); 50175b0747a1f55329109e84c9a1322dcb95e2848Mark Salyzyn * you may not use this file except in compliance with the License. 60175b0747a1f55329109e84c9a1322dcb95e2848Mark Salyzyn * You may obtain a copy of the License at 70175b0747a1f55329109e84c9a1322dcb95e2848Mark Salyzyn * 80175b0747a1f55329109e84c9a1322dcb95e2848Mark Salyzyn * http://www.apache.org/licenses/LICENSE-2.0 90175b0747a1f55329109e84c9a1322dcb95e2848Mark Salyzyn * 100175b0747a1f55329109e84c9a1322dcb95e2848Mark Salyzyn * Unless required by applicable law or agreed to in writing, software 110175b0747a1f55329109e84c9a1322dcb95e2848Mark Salyzyn * distributed under the License is distributed on an "AS IS" BASIS, 120175b0747a1f55329109e84c9a1322dcb95e2848Mark Salyzyn * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 130175b0747a1f55329109e84c9a1322dcb95e2848Mark Salyzyn * See the License for the specific language governing permissions and 140175b0747a1f55329109e84c9a1322dcb95e2848Mark Salyzyn * limitations under the License. 150175b0747a1f55329109e84c9a1322dcb95e2848Mark Salyzyn */ 160175b0747a1f55329109e84c9a1322dcb95e2848Mark Salyzyn 17fa3716b2501ccddc8e0cd30f6343692b8deb7639Mark Salyzyn#include <ctype.h> 180175b0747a1f55329109e84c9a1322dcb95e2848Mark Salyzyn#include <poll.h> 198daa9af02dc0e63ce220e3fa95bf5fe4d6b7a99aMark Salyzyn#include <sys/prctl.h> 200175b0747a1f55329109e84c9a1322dcb95e2848Mark Salyzyn#include <sys/socket.h> 215c77ad55d0cdee84cd45fd5d0d066f3c61d76ce6Mark Salyzyn#include <sys/types.h> 22dfc47e86858ea67c72f1df2fdb97094b8e8248f2Mark Salyzyn 230175b0747a1f55329109e84c9a1322dcb95e2848Mark Salyzyn#include <cutils/sockets.h> 24aeaaf81c2cc8366ac4f66eb3d2fc85f9b8194982Mark Salyzyn#include <private/android_logger.h> 250175b0747a1f55329109e84c9a1322dcb95e2848Mark Salyzyn 260175b0747a1f55329109e84c9a1322dcb95e2848Mark Salyzyn#include "FlushCommand.h" 272ad0bd0a9b594bbe2560b405b0008b7bc742cfcaMark Salyzyn#include "LogBuffer.h" 282ad0bd0a9b594bbe2560b405b0008b7bc742cfcaMark Salyzyn#include "LogBufferElement.h" 292ad0bd0a9b594bbe2560b405b0008b7bc742cfcaMark Salyzyn#include "LogReader.h" 302ad0bd0a9b594bbe2560b405b0008b7bc742cfcaMark Salyzyn#include "LogUtils.h" 310175b0747a1f55329109e84c9a1322dcb95e2848Mark Salyzyn 32501c373916e292764400dbae735f44b33378400fMark SalyzynLogReader::LogReader(LogBuffer* logbuf) 33501c373916e292764400dbae735f44b33378400fMark Salyzyn : SocketListener(getLogSocket(), true), mLogbuf(*logbuf) { 347718778793b106498b931dd708a466cf3a6f6a0fMark Salyzyn} 350175b0747a1f55329109e84c9a1322dcb95e2848Mark Salyzyn 360175b0747a1f55329109e84c9a1322dcb95e2848Mark Salyzyn// When we are notified a new log entry is available, inform 370175b0747a1f55329109e84c9a1322dcb95e2848Mark Salyzyn// all of our listening sockets. 380175b0747a1f55329109e84c9a1322dcb95e2848Mark Salyzynvoid LogReader::notifyNewLog() { 390175b0747a1f55329109e84c9a1322dcb95e2848Mark Salyzyn FlushCommand command(*this); 400175b0747a1f55329109e84c9a1322dcb95e2848Mark Salyzyn runOnEachSocket(&command); 410175b0747a1f55329109e84c9a1322dcb95e2848Mark Salyzyn} 420175b0747a1f55329109e84c9a1322dcb95e2848Mark Salyzyn 43501c373916e292764400dbae735f44b33378400fMark Salyzynbool LogReader::onDataAvailable(SocketClient* cli) { 44e3aeeeeccc260c29ca5907a444f8d746bcc2f8a5Mark Salyzyn static bool name_set; 45e3aeeeeccc260c29ca5907a444f8d746bcc2f8a5Mark Salyzyn if (!name_set) { 46e3aeeeeccc260c29ca5907a444f8d746bcc2f8a5Mark Salyzyn prctl(PR_SET_NAME, "logd.reader"); 47e3aeeeeccc260c29ca5907a444f8d746bcc2f8a5Mark Salyzyn name_set = true; 48e3aeeeeccc260c29ca5907a444f8d746bcc2f8a5Mark Salyzyn } 498daa9af02dc0e63ce220e3fa95bf5fe4d6b7a99aMark Salyzyn 500175b0747a1f55329109e84c9a1322dcb95e2848Mark Salyzyn char buffer[255]; 510175b0747a1f55329109e84c9a1322dcb95e2848Mark Salyzyn 520175b0747a1f55329109e84c9a1322dcb95e2848Mark Salyzyn int len = read(cli->getSocket(), buffer, sizeof(buffer) - 1); 530175b0747a1f55329109e84c9a1322dcb95e2848Mark Salyzyn if (len <= 0) { 540175b0747a1f55329109e84c9a1322dcb95e2848Mark Salyzyn doSocketDelete(cli); 550175b0747a1f55329109e84c9a1322dcb95e2848Mark Salyzyn return false; 560175b0747a1f55329109e84c9a1322dcb95e2848Mark Salyzyn } 570175b0747a1f55329109e84c9a1322dcb95e2848Mark Salyzyn buffer[len] = '\0'; 580175b0747a1f55329109e84c9a1322dcb95e2848Mark Salyzyn 590175b0747a1f55329109e84c9a1322dcb95e2848Mark Salyzyn unsigned long tail = 0; 600175b0747a1f55329109e84c9a1322dcb95e2848Mark Salyzyn static const char _tail[] = " tail="; 61501c373916e292764400dbae735f44b33378400fMark Salyzyn char* cp = strstr(buffer, _tail); 620175b0747a1f55329109e84c9a1322dcb95e2848Mark Salyzyn if (cp) { 630175b0747a1f55329109e84c9a1322dcb95e2848Mark Salyzyn tail = atol(cp + sizeof(_tail) - 1); 640175b0747a1f55329109e84c9a1322dcb95e2848Mark Salyzyn } 650175b0747a1f55329109e84c9a1322dcb95e2848Mark Salyzyn 66fa3716b2501ccddc8e0cd30f6343692b8deb7639Mark Salyzyn log_time start(log_time::EPOCH); 67fa3716b2501ccddc8e0cd30f6343692b8deb7639Mark Salyzyn static const char _start[] = " start="; 68fa3716b2501ccddc8e0cd30f6343692b8deb7639Mark Salyzyn cp = strstr(buffer, _start); 69fa3716b2501ccddc8e0cd30f6343692b8deb7639Mark Salyzyn if (cp) { 70fa3716b2501ccddc8e0cd30f6343692b8deb7639Mark Salyzyn // Parse errors will result in current time 71fa3716b2501ccddc8e0cd30f6343692b8deb7639Mark Salyzyn start.strptime(cp + sizeof(_start) - 1, "%s.%q"); 72fa3716b2501ccddc8e0cd30f6343692b8deb7639Mark Salyzyn } 73fa3716b2501ccddc8e0cd30f6343692b8deb7639Mark Salyzyn 74b75cce0389748bea111ca62af623645117e12d9dMark Salyzyn uint64_t timeout = 0; 75b75cce0389748bea111ca62af623645117e12d9dMark Salyzyn static const char _timeout[] = " timeout="; 76b75cce0389748bea111ca62af623645117e12d9dMark Salyzyn cp = strstr(buffer, _timeout); 77b75cce0389748bea111ca62af623645117e12d9dMark Salyzyn if (cp) { 78b75cce0389748bea111ca62af623645117e12d9dMark Salyzyn timeout = atol(cp + sizeof(_timeout) - 1) * NS_PER_SEC + 79b75cce0389748bea111ca62af623645117e12d9dMark Salyzyn log_time(CLOCK_REALTIME).nsec(); 80b75cce0389748bea111ca62af623645117e12d9dMark Salyzyn } 81b75cce0389748bea111ca62af623645117e12d9dMark Salyzyn 820175b0747a1f55329109e84c9a1322dcb95e2848Mark Salyzyn unsigned int logMask = -1; 830175b0747a1f55329109e84c9a1322dcb95e2848Mark Salyzyn static const char _logIds[] = " lids="; 840175b0747a1f55329109e84c9a1322dcb95e2848Mark Salyzyn cp = strstr(buffer, _logIds); 850175b0747a1f55329109e84c9a1322dcb95e2848Mark Salyzyn if (cp) { 860175b0747a1f55329109e84c9a1322dcb95e2848Mark Salyzyn logMask = 0; 870175b0747a1f55329109e84c9a1322dcb95e2848Mark Salyzyn cp += sizeof(_logIds) - 1; 880175b0747a1f55329109e84c9a1322dcb95e2848Mark Salyzyn while (*cp && *cp != '\0') { 890175b0747a1f55329109e84c9a1322dcb95e2848Mark Salyzyn int val = 0; 90fa3716b2501ccddc8e0cd30f6343692b8deb7639Mark Salyzyn while (isdigit(*cp)) { 91fa3716b2501ccddc8e0cd30f6343692b8deb7639Mark Salyzyn val = val * 10 + *cp - '0'; 920175b0747a1f55329109e84c9a1322dcb95e2848Mark Salyzyn ++cp; 930175b0747a1f55329109e84c9a1322dcb95e2848Mark Salyzyn } 940175b0747a1f55329109e84c9a1322dcb95e2848Mark Salyzyn logMask |= 1 << val; 950175b0747a1f55329109e84c9a1322dcb95e2848Mark Salyzyn if (*cp != ',') { 960175b0747a1f55329109e84c9a1322dcb95e2848Mark Salyzyn break; 970175b0747a1f55329109e84c9a1322dcb95e2848Mark Salyzyn } 980175b0747a1f55329109e84c9a1322dcb95e2848Mark Salyzyn ++cp; 990175b0747a1f55329109e84c9a1322dcb95e2848Mark Salyzyn } 1000175b0747a1f55329109e84c9a1322dcb95e2848Mark Salyzyn } 1010175b0747a1f55329109e84c9a1322dcb95e2848Mark Salyzyn 1020175b0747a1f55329109e84c9a1322dcb95e2848Mark Salyzyn pid_t pid = 0; 1030175b0747a1f55329109e84c9a1322dcb95e2848Mark Salyzyn static const char _pid[] = " pid="; 1040175b0747a1f55329109e84c9a1322dcb95e2848Mark Salyzyn cp = strstr(buffer, _pid); 1050175b0747a1f55329109e84c9a1322dcb95e2848Mark Salyzyn if (cp) { 1060175b0747a1f55329109e84c9a1322dcb95e2848Mark Salyzyn pid = atol(cp + sizeof(_pid) - 1); 1070175b0747a1f55329109e84c9a1322dcb95e2848Mark Salyzyn } 1080175b0747a1f55329109e84c9a1322dcb95e2848Mark Salyzyn 1090175b0747a1f55329109e84c9a1322dcb95e2848Mark Salyzyn bool nonBlock = false; 1100eeb06b932f185e10377e4494475d2cdd6adfa1bMark Salyzyn if (!fastcmp<strncmp>(buffer, "dumpAndClose", 12)) { 111f669acb01880216b6c1d29fc226f2c3ec3a6368aMark Salyzyn // Allow writer to get some cycles, and wait for pending notifications 112f669acb01880216b6c1d29fc226f2c3ec3a6368aMark Salyzyn sched_yield(); 113f669acb01880216b6c1d29fc226f2c3ec3a6368aMark Salyzyn LogTimeEntry::lock(); 114f669acb01880216b6c1d29fc226f2c3ec3a6368aMark Salyzyn LogTimeEntry::unlock(); 115f669acb01880216b6c1d29fc226f2c3ec3a6368aMark Salyzyn sched_yield(); 1160175b0747a1f55329109e84c9a1322dcb95e2848Mark Salyzyn nonBlock = true; 1170175b0747a1f55329109e84c9a1322dcb95e2848Mark Salyzyn } 1180175b0747a1f55329109e84c9a1322dcb95e2848Mark Salyzyn 119f7c0f75275d0fde2d8b7614f1501f0ad0cd3a01cMark Salyzyn uint64_t sequence = 1; 120f7c0f75275d0fde2d8b7614f1501f0ad0cd3a01cMark Salyzyn // Convert realtime to sequence number 121f7c0f75275d0fde2d8b7614f1501f0ad0cd3a01cMark Salyzyn if (start != log_time::EPOCH) { 122a1c60cf80d0d1002576a6cf8aa395b295c6a272eMark Salyzyn class LogFindStart { 123a1c60cf80d0d1002576a6cf8aa395b295c6a272eMark Salyzyn const pid_t mPid; 124a1c60cf80d0d1002576a6cf8aa395b295c6a272eMark Salyzyn const unsigned mLogMask; 125a1c60cf80d0d1002576a6cf8aa395b295c6a272eMark Salyzyn bool startTimeSet; 126501c373916e292764400dbae735f44b33378400fMark Salyzyn log_time& start; 127501c373916e292764400dbae735f44b33378400fMark Salyzyn uint64_t& sequence; 128f7c0f75275d0fde2d8b7614f1501f0ad0cd3a01cMark Salyzyn uint64_t last; 129b6bee33182cedea49199eb2252b3f3b442899c6dMark Salyzyn bool isMonotonic; 130a1c60cf80d0d1002576a6cf8aa395b295c6a272eMark Salyzyn 131501c373916e292764400dbae735f44b33378400fMark Salyzyn public: 132501c373916e292764400dbae735f44b33378400fMark Salyzyn LogFindStart(unsigned logMask, pid_t pid, log_time& start, 133501c373916e292764400dbae735f44b33378400fMark Salyzyn uint64_t& sequence, bool isMonotonic) 134501c373916e292764400dbae735f44b33378400fMark Salyzyn : mPid(pid), 135501c373916e292764400dbae735f44b33378400fMark Salyzyn mLogMask(logMask), 136501c373916e292764400dbae735f44b33378400fMark Salyzyn startTimeSet(false), 137501c373916e292764400dbae735f44b33378400fMark Salyzyn start(start), 138501c373916e292764400dbae735f44b33378400fMark Salyzyn sequence(sequence), 139501c373916e292764400dbae735f44b33378400fMark Salyzyn last(sequence), 140501c373916e292764400dbae735f44b33378400fMark Salyzyn isMonotonic(isMonotonic) { 1417718778793b106498b931dd708a466cf3a6f6a0fMark Salyzyn } 142a1c60cf80d0d1002576a6cf8aa395b295c6a272eMark Salyzyn 143501c373916e292764400dbae735f44b33378400fMark Salyzyn static int callback(const LogBufferElement* element, void* obj) { 144501c373916e292764400dbae735f44b33378400fMark Salyzyn LogFindStart* me = reinterpret_cast<LogFindStart*>(obj); 145501c373916e292764400dbae735f44b33378400fMark Salyzyn if ((!me->mPid || (me->mPid == element->getPid())) && 146501c373916e292764400dbae735f44b33378400fMark Salyzyn (me->mLogMask & (1 << element->getLogId()))) { 147a1c60cf80d0d1002576a6cf8aa395b295c6a272eMark Salyzyn if (me->start == element->getRealTime()) { 148f7c0f75275d0fde2d8b7614f1501f0ad0cd3a01cMark Salyzyn me->sequence = element->getSequence(); 149a1c60cf80d0d1002576a6cf8aa395b295c6a272eMark Salyzyn me->startTimeSet = true; 150f7c0f75275d0fde2d8b7614f1501f0ad0cd3a01cMark Salyzyn return -1; 151b6bee33182cedea49199eb2252b3f3b442899c6dMark Salyzyn } else if (!me->isMonotonic || 152501c373916e292764400dbae735f44b33378400fMark Salyzyn android::isMonotonic(element->getRealTime())) { 153a1c60cf80d0d1002576a6cf8aa395b295c6a272eMark Salyzyn if (me->start < element->getRealTime()) { 154f7c0f75275d0fde2d8b7614f1501f0ad0cd3a01cMark Salyzyn me->sequence = me->last; 155a1c60cf80d0d1002576a6cf8aa395b295c6a272eMark Salyzyn me->startTimeSet = true; 156f7c0f75275d0fde2d8b7614f1501f0ad0cd3a01cMark Salyzyn return -1; 157a1c60cf80d0d1002576a6cf8aa395b295c6a272eMark Salyzyn } 158f7c0f75275d0fde2d8b7614f1501f0ad0cd3a01cMark Salyzyn me->last = element->getSequence(); 159b6bee33182cedea49199eb2252b3f3b442899c6dMark Salyzyn } else { 160b6bee33182cedea49199eb2252b3f3b442899c6dMark Salyzyn me->last = element->getSequence(); 161a1c60cf80d0d1002576a6cf8aa395b295c6a272eMark Salyzyn } 162a1c60cf80d0d1002576a6cf8aa395b295c6a272eMark Salyzyn } 163a1c60cf80d0d1002576a6cf8aa395b295c6a272eMark Salyzyn return false; 164a1c60cf80d0d1002576a6cf8aa395b295c6a272eMark Salyzyn } 165a1c60cf80d0d1002576a6cf8aa395b295c6a272eMark Salyzyn 166501c373916e292764400dbae735f44b33378400fMark Salyzyn bool found() { 167501c373916e292764400dbae735f44b33378400fMark Salyzyn return startTimeSet; 168501c373916e292764400dbae735f44b33378400fMark Salyzyn } 169b6bee33182cedea49199eb2252b3f3b442899c6dMark Salyzyn } logFindStart(logMask, pid, start, sequence, 170b6bee33182cedea49199eb2252b3f3b442899c6dMark Salyzyn logbuf().isMonotonic() && android::isMonotonic(start)); 171a1c60cf80d0d1002576a6cf8aa395b295c6a272eMark Salyzyn 172f7c0f75275d0fde2d8b7614f1501f0ad0cd3a01cMark Salyzyn logbuf().flushTo(cli, sequence, FlushCommand::hasReadLogs(cli), 1738fa8896d2ed97eb274c62f0e386dabf2e2a82a45Mark Salyzyn FlushCommand::hasSecurityLogs(cli), 174a1c60cf80d0d1002576a6cf8aa395b295c6a272eMark Salyzyn logFindStart.callback, &logFindStart); 175a1c60cf80d0d1002576a6cf8aa395b295c6a272eMark Salyzyn 176a1c60cf80d0d1002576a6cf8aa395b295c6a272eMark Salyzyn if (!logFindStart.found()) { 177a1c60cf80d0d1002576a6cf8aa395b295c6a272eMark Salyzyn if (nonBlock) { 178a1c60cf80d0d1002576a6cf8aa395b295c6a272eMark Salyzyn doSocketDelete(cli); 179a1c60cf80d0d1002576a6cf8aa395b295c6a272eMark Salyzyn return false; 180a1c60cf80d0d1002576a6cf8aa395b295c6a272eMark Salyzyn } 181f7c0f75275d0fde2d8b7614f1501f0ad0cd3a01cMark Salyzyn sequence = LogBufferElement::getCurrentSequence(); 182a1c60cf80d0d1002576a6cf8aa395b295c6a272eMark Salyzyn } 183fa3716b2501ccddc8e0cd30f6343692b8deb7639Mark Salyzyn } 184fa3716b2501ccddc8e0cd30f6343692b8deb7639Mark Salyzyn 185b75cce0389748bea111ca62af623645117e12d9dMark Salyzyn FlushCommand command(*this, nonBlock, tail, logMask, pid, sequence, timeout); 1865c77ad55d0cdee84cd45fd5d0d066f3c61d76ce6Mark Salyzyn 1875c77ad55d0cdee84cd45fd5d0d066f3c61d76ce6Mark Salyzyn // Set acceptable upper limit to wait for slow reader processing b/27242723 1885c77ad55d0cdee84cd45fd5d0d066f3c61d76ce6Mark Salyzyn struct timeval t = { LOGD_SNDTIMEO, 0 }; 189501c373916e292764400dbae735f44b33378400fMark Salyzyn setsockopt(cli->getSocket(), SOL_SOCKET, SO_SNDTIMEO, (const char*)&t, 190501c373916e292764400dbae735f44b33378400fMark Salyzyn sizeof(t)); 1915c77ad55d0cdee84cd45fd5d0d066f3c61d76ce6Mark Salyzyn 1920175b0747a1f55329109e84c9a1322dcb95e2848Mark Salyzyn command.runSocketCommand(cli); 1930175b0747a1f55329109e84c9a1322dcb95e2848Mark Salyzyn return true; 1940175b0747a1f55329109e84c9a1322dcb95e2848Mark Salyzyn} 1950175b0747a1f55329109e84c9a1322dcb95e2848Mark Salyzyn 196501c373916e292764400dbae735f44b33378400fMark Salyzynvoid LogReader::doSocketDelete(SocketClient* cli) { 197501c373916e292764400dbae735f44b33378400fMark Salyzyn LastLogTimes& times = mLogbuf.mTimes; 1980175b0747a1f55329109e84c9a1322dcb95e2848Mark Salyzyn LogTimeEntry::lock(); 1990175b0747a1f55329109e84c9a1322dcb95e2848Mark Salyzyn LastLogTimes::iterator it = times.begin(); 200501c373916e292764400dbae735f44b33378400fMark Salyzyn while (it != times.end()) { 201501c373916e292764400dbae735f44b33378400fMark Salyzyn LogTimeEntry* entry = (*it); 2020175b0747a1f55329109e84c9a1322dcb95e2848Mark Salyzyn if (entry->mClient == cli) { 2030175b0747a1f55329109e84c9a1322dcb95e2848Mark Salyzyn times.erase(it); 2040175b0747a1f55329109e84c9a1322dcb95e2848Mark Salyzyn entry->release_Locked(); 2050175b0747a1f55329109e84c9a1322dcb95e2848Mark Salyzyn break; 2060175b0747a1f55329109e84c9a1322dcb95e2848Mark Salyzyn } 2070175b0747a1f55329109e84c9a1322dcb95e2848Mark Salyzyn it++; 2080175b0747a1f55329109e84c9a1322dcb95e2848Mark Salyzyn } 2090175b0747a1f55329109e84c9a1322dcb95e2848Mark Salyzyn LogTimeEntry::unlock(); 2100175b0747a1f55329109e84c9a1322dcb95e2848Mark Salyzyn} 211dfc47e86858ea67c72f1df2fdb97094b8e8248f2Mark Salyzyn 212dfc47e86858ea67c72f1df2fdb97094b8e8248f2Mark Salyzynint LogReader::getLogSocket() { 213dfc47e86858ea67c72f1df2fdb97094b8e8248f2Mark Salyzyn static const char socketName[] = "logdr"; 214dfc47e86858ea67c72f1df2fdb97094b8e8248f2Mark Salyzyn int sock = android_get_control_socket(socketName); 215dfc47e86858ea67c72f1df2fdb97094b8e8248f2Mark Salyzyn 216dfc47e86858ea67c72f1df2fdb97094b8e8248f2Mark Salyzyn if (sock < 0) { 217501c373916e292764400dbae735f44b33378400fMark Salyzyn sock = socket_local_server( 218501c373916e292764400dbae735f44b33378400fMark Salyzyn socketName, ANDROID_SOCKET_NAMESPACE_RESERVED, SOCK_SEQPACKET); 219dfc47e86858ea67c72f1df2fdb97094b8e8248f2Mark Salyzyn } 220dfc47e86858ea67c72f1df2fdb97094b8e8248f2Mark Salyzyn 221dfc47e86858ea67c72f1df2fdb97094b8e8248f2Mark Salyzyn return sock; 222dfc47e86858ea67c72f1df2fdb97094b8e8248f2Mark Salyzyn} 223