------------------------------------------------------------ revno: 2781 committer: Jacek Sieka <arnethed...@gmail.com> branch nick: dcplusplus timestamp: Sun 2012-01-01 15:22:32 +0100 message: Use FileReader for hashmanager, remove fasthash option modified: dcpp/File.cpp dcpp/FileReader.cpp dcpp/FileReader.h dcpp/HashManager.cpp dcpp/SettingsManager.cpp dcpp/SettingsManager.h utils/xsum.cpp win32/AdvancedPage.cpp
-- lp:dcplusplus https://code.launchpad.net/~dcplusplus-team/dcplusplus/trunk Your team Dcplusplus-team is subscribed to branch lp:dcplusplus. To unsubscribe from this branch go to https://code.launchpad.net/~dcplusplus-team/dcplusplus/trunk/+edit-subscription
=== modified file 'dcpp/File.cpp' --- dcpp/File.cpp 2011-04-13 19:16:51 +0000 +++ dcpp/File.cpp 2012-01-01 14:22:32 +0000 @@ -80,13 +80,12 @@ } int64_t File::getSize() noexcept { - DWORD x; - DWORD l = ::GetFileSize(h, &x); - - if( (l == INVALID_FILE_SIZE) && (GetLastError() != NO_ERROR)) + LARGE_INTEGER x; + if(!::GetFileSizeEx(h, &x)) { return -1; + } - return (int64_t)l | ((int64_t)x)<<32; + return x.QuadPart; } int64_t File::getPos() noexcept { LONG x = 0; === modified file 'dcpp/FileReader.cpp' --- dcpp/FileReader.cpp 2011-12-31 15:13:51 +0000 +++ dcpp/FileReader.cpp 2012-01-01 14:22:32 +0000 @@ -43,8 +43,9 @@ size_t total = 0; size_t n = buffer.size(); - while(f.read(buf, n) > 0) { - callback(buf, n); + bool go = true; + while(f.read(buf, n) > 0 && go) { + go = callback(buf, n); total += n; n = buffer.size(); } @@ -129,13 +130,14 @@ } over.Offset = hn; - for (; hn == bufSize;) { + bool go = true; + for (; hn == bufSize && go;) { // Start a new overlapped read res = ::ReadFile(h, rbuf, bufSize, NULL, &over); auto err = ::GetLastError(); // Process the previously read data - callback(hbuf, hn); + go = callback(hbuf, hn); if (!res && err != ERROR_IO_PENDING) { if(err != ERROR_HANDLE_EOF) { @@ -201,14 +203,15 @@ auto blockSize = getBlockSize(si.dwPageSize); LARGE_INTEGER total = { 0 }; - while(size.QuadPart > 0) { + bool go = true; + while(size.QuadPart > 0 && go) { auto n = min(size.QuadPart, (int64_t)blockSize); auto p = ::MapViewOfFile(hmap, FILE_MAP_READ, total.HighPart, total.LowPart, static_cast<DWORD>(n)); if(!p) { throw FileException(Util::translateError(::GetLastError())); } - callback(p, n); + go = callback(p, n); if(!::UnmapViewOfFile(p)) { throw FileException(Util::translateError(::GetLastError())); @@ -223,12 +226,98 @@ #else +#include <sys/mman.h> // mmap, munmap, madvise +#include <signal.h> // for handling read errors from previous trio +#include <setjmp.h> + size_t FileReader::readDirect(const string& file, const DataCallback& callback) { return READ_FAILED; } -size_t FileReader::readMapped(const string& file, const DataCallback& callback) { - return READ_FAILED; +static const int64_t BUF_SIZE = 0x1000000 - (0x1000000 % getpagesize()); +static sigjmp_buf sb_env; + +static void sigbus_handler(int signum, siginfo_t* info, void* context) { + // Jump back to the readMapped which will return error. Apparently truncating + // a file in Solaris sets si_code to BUS_OBJERR + if (signum == SIGBUS && (info->si_code == BUS_ADRERR || info->si_code == BUS_OBJERR)) + siglongjmp(sb_env, 1); +} + +size_t FileReader::readMapped(const string& filename, const DataCallback& callback) { + int fd = open(Text::fromUtf8(filename).c_str(), O_RDONLY); + if(fd == -1) { + dcdebug("Error opening file %s: %s\n", filename.c_str(), Util::translateError(errno).c_str()); + return READ_FAILED; + } + + int64_t pos = 0; + auto size =0; + + // Prepare and setup a signal handler in case of SIGBUS during mmapped file reads. + // SIGBUS can be sent when the file is truncated or in case of read errors. + struct sigaction act, oldact; + sigset_t signalset; + + sigemptyset(&signalset); + + act.sa_handler = NULL; + act.sa_sigaction = sigbus_handler; + act.sa_mask = signalset; + act.sa_flags = SA_SIGINFO | SA_RESETHAND; + + if (sigaction(SIGBUS, &act, &oldact) == -1) { + dcdebug("Failed to set signal handler for fastHash\n"); + close(fd); + return READ_FAILED; // Better luck with the slow hash. + } + + void* buf = NULL; + int64_t size_read = 0; + + uint64_t lastRead = GET_TICK(); + while (pos < size) { + size_read = std::min(size - pos, BUF_SIZE); + buf = mmap(0, size_read, PROT_READ, MAP_SHARED, fd, pos); + if (buf == MAP_FAILED) { + dcdebug("Error calling mmap for file %s: %s\n", filename.c_str(), Util::translateError(errno).c_str()); + break; + } + + if (sigsetjmp(sb_env, 1)) { + dcdebug("Caught SIGBUS for file %s\n", filename.c_str()); + break; + } + + if (posix_madvise(buf, size_read, POSIX_MADV_SEQUENTIAL | POSIX_MADV_WILLNEED) == -1) { + dcdebug("Error calling madvise for file %s: %s\n", filename.c_str(), Util::translateError(errno).c_str()); + break; + } + + if(!callback(buf, size_read)) { + break; + } + + if (munmap(buf, size_read) == -1) { + dcdebug("Error calling munmap for file %s: %s\n", filename.c_str(), Util::translateError(errno).c_str()); + break; + } + + buf = NULL; + pos += size_read; + } + + if (buf != NULL && buf != MAP_FAILED && munmap(buf, size_read) == -1) { + dcdebug("Error calling munmap for file %s: %s\n", filename.c_str(), Util::translateError(errno).c_str()); + } + + ::close(fd); + + if (sigaction(SIGBUS, &oldact, NULL) == -1) { + dcdebug("Failed to reset old signal handler for SIGBUS\n"); + } + + return pos == size ? pos : READ_FAILED; } #endif === modified file 'dcpp/FileReader.h' --- dcpp/FileReader.h 2011-12-31 15:13:51 +0000 +++ dcpp/FileReader.h 2012-01-01 14:22:32 +0000 @@ -43,7 +43,7 @@ CACHED }; - typedef function<void(void*, size_t)> DataCallback; + typedef function<bool(const void*, size_t)> DataCallback; /** * Set up file reader @@ -60,6 +60,7 @@ * @throw FileException if the read fails */ size_t read(const string& file, const DataCallback& callback); + private: static const size_t DEFAULT_BLOCK_SIZE = 64*1024; static const size_t DEFAULT_MMAP_SIZE = 64*1024*1024; === modified file 'dcpp/HashManager.cpp' --- dcpp/HashManager.cpp 2011-12-22 22:14:45 +0000 +++ dcpp/HashManager.cpp 2012-01-01 14:22:32 +0000 @@ -24,15 +24,10 @@ #include "SimpleXML.h" #include "LogManager.h" #include "File.h" +#include "FileReader.h" #include "ZUtils.h" #include "SFVReader.h" -#ifndef _WIN32 -#include <sys/mman.h> // mmap, munmap, madvise -#include <signal.h> // for handling read errors from previous trio -#include <setjmp.h> -#endif - namespace dcpp { using std::swap; @@ -384,7 +379,6 @@ static const string sType = "Type"; static const string sTTH = "TTH"; static const string sIndex = "Index"; -static const string sLeafSize = "LeafSize"; // Residue from v1 as well static const string sBlockSize = "BlockSize"; static const string sTimeStamp = "TimeStamp"; static const string sRoot = "Root"; @@ -533,236 +527,11 @@ s.wait(); } -#ifdef _WIN32 -#define BUF_SIZE (256*1024) - -bool HashManager::Hasher::fastHash(const string& fname, uint8_t* buf, TigerTree& tth, int64_t size, CRC32Filter* xcrc32) { - HANDLE h = INVALID_HANDLE_VALUE; - DWORD x, y; - if (!GetDiskFreeSpace(Text::toT(Util::getFilePath(fname)).c_str(), &y, &x, &y, &y)) { - return false; - } else { - if ((BUF_SIZE % x) != 0) { - return false; - } else { - h = ::CreateFile(Text::toT(fname).c_str(), GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, - FILE_FLAG_NO_BUFFERING | FILE_FLAG_OVERLAPPED, NULL); - if (h == INVALID_HANDLE_VALUE) - return false; - } - } - DWORD hn = 0; - DWORD rn = 0; - uint8_t* hbuf = buf + BUF_SIZE; - uint8_t* rbuf = buf; - - OVERLAPPED over = { 0 }; - BOOL res = TRUE; - over.hEvent = CreateEvent(NULL, FALSE, TRUE, NULL); - - bool ok = false; - - uint64_t lastRead = GET_TICK(); - if (!::ReadFile(h, hbuf, BUF_SIZE, &hn, &over)) { - if (GetLastError() == ERROR_HANDLE_EOF) { - hn = 0; - } else if (GetLastError() == ERROR_IO_PENDING) { - if (!GetOverlappedResult(h, &over, &hn, TRUE)) { - if (GetLastError() == ERROR_HANDLE_EOF) { - hn = 0; - } else { - goto cleanup; - } - } - } else { - goto cleanup; - } - } - - over.Offset = hn; - size -= hn; - while (!stop) { - if (size > 0) { - // Start a new overlapped read - ResetEvent(over.hEvent); - if (SETTING(MAX_HASH_SPEED) > 0) { - uint64_t now = GET_TICK(); - uint64_t minTime = hn * 1000LL / (SETTING(MAX_HASH_SPEED) * 1024LL * 1024LL); - if (lastRead + minTime > now) { - uint64_t diff = now - lastRead; - Thread::sleep(minTime - diff); - } - lastRead = lastRead + minTime; - } else { - lastRead = GET_TICK(); - } - res = ReadFile(h, rbuf, BUF_SIZE, &rn, &over); - } else { - rn = 0; - } - - tth.update(hbuf, hn); - if (xcrc32) - (*xcrc32)(hbuf, hn); - - { - Lock l(cs); - currentSize = max(currentSize - hn, _LL(0)); - } - - if (size == 0) { - ok = true; - break; - } - - if (!res) { - // deal with the error code - switch (GetLastError()) { - case ERROR_IO_PENDING: - if (!GetOverlappedResult(h, &over, &rn, TRUE)) { - dcdebug("Error 0x%x: %s\n", GetLastError(), Util::translateError(GetLastError()).c_str()); - goto cleanup; - } - break; - default: - dcdebug("Error 0x%x: %s\n", GetLastError(), Util::translateError(GetLastError()).c_str()); - goto cleanup; - } - } - - instantPause(); - - *((uint64_t*)&over.Offset) += rn; - size -= rn; - - swap(rbuf, hbuf); - swap(rn, hn); - } - - cleanup: - ::CloseHandle(over.hEvent); - ::CloseHandle(h); - return ok; -} - -#else // !_WIN32 -static const int64_t BUF_SIZE = 0x1000000 - (0x1000000 % getpagesize()); -static sigjmp_buf sb_env; - -static void sigbus_handler(int signum, siginfo_t* info, void* context) { - // Jump back to the fastHash which will return error. Apparently truncating - // a file in Solaris sets si_code to BUS_OBJERR - if (signum == SIGBUS && (info->si_code == BUS_ADRERR || info->si_code == BUS_OBJERR)) - siglongjmp(sb_env, 1); -} - -bool HashManager::Hasher::fastHash(const string& filename, uint8_t* , TigerTree& tth, int64_t size, CRC32Filter* xcrc32) { - int fd = open(Text::fromUtf8(filename).c_str(), O_RDONLY); - if(fd == -1) { - dcdebug("Error opening file %s: %s\n", filename.c_str(), Util::translateError(errno).c_str()); - return false; - } - - int64_t pos = 0; - int64_t size_read = 0; - void *buf = NULL; - bool ok = false; - - // Prepare and setup a signal handler in case of SIGBUS during mmapped file reads. - // SIGBUS can be sent when the file is truncated or in case of read errors. - struct sigaction act, oldact; - sigset_t signalset; - - sigemptyset(&signalset); - - act.sa_handler = NULL; - act.sa_sigaction = sigbus_handler; - act.sa_mask = signalset; - act.sa_flags = SA_SIGINFO | SA_RESETHAND; - - if (sigaction(SIGBUS, &act, &oldact) == -1) { - dcdebug("Failed to set signal handler for fastHash\n"); - close(fd); - return false; // Better luck with the slow hash. - } - - uint64_t lastRead = GET_TICK(); - while (pos < size && !stop) { - size_read = std::min(size - pos, BUF_SIZE); - buf = mmap(0, size_read, PROT_READ, MAP_SHARED, fd, pos); - if (buf == MAP_FAILED) { - dcdebug("Error calling mmap for file %s: %s\n", filename.c_str(), Util::translateError(errno).c_str()); - break; - } - - if (sigsetjmp(sb_env, 1)) { - dcdebug("Caught SIGBUS for file %s\n", filename.c_str()); - break; - } - - if (posix_madvise(buf, size_read, POSIX_MADV_SEQUENTIAL | POSIX_MADV_WILLNEED) == -1) { - dcdebug("Error calling madvise for file %s: %s\n", filename.c_str(), Util::translateError(errno).c_str()); - break; - } - - if (SETTING(MAX_HASH_SPEED) > 0) { - uint64_t now = GET_TICK(); - uint64_t minTime = size_read * 1000LL / (SETTING(MAX_HASH_SPEED) * 1024LL * 1024LL); - if (lastRead + minTime > now) { - uint64_t diff = now - lastRead; - Thread::sleep(minTime - diff); - } - lastRead = lastRead + minTime; - } else { - lastRead = GET_TICK(); - } - - tth.update(buf, size_read); - if (xcrc32) - (*xcrc32)(buf, size_read); - - { - Lock l(cs); - currentSize = max(static_cast<uint64_t>(currentSize - size_read), static_cast<uint64_t>(0)); - } - - if (munmap(buf, size_read) == -1) { - dcdebug("Error calling munmap for file %s: %s\n", filename.c_str(), Util::translateError(errno).c_str()); - break; - } - - buf = NULL; - pos += size_read; - - instantPause(); - - if (pos == size) { - ok = true; - } - } - - if (buf != NULL && buf != MAP_FAILED && munmap(buf, size_read) == -1) { - dcdebug("Error calling munmap for file %s: %s\n", filename.c_str(), Util::translateError(errno).c_str()); - } - - close(fd); - - if (sigaction(SIGBUS, &oldact, NULL) == -1) { - dcdebug("Failed to reset old signal handler for SIGBUS\n"); - } - - return ok; -} - -#endif // !_WIN32 int HashManager::Hasher::run() { setThreadPriority(Thread::IDLE); - uint8_t* buf = NULL; - bool virtualBuf = true; - string fname; - bool last = false; + for(;;) { s.wait(); if(stop) @@ -779,34 +548,24 @@ currentFile = fname = w.begin()->first; currentSize = w.begin()->second; w.erase(w.begin()); - last = w.empty(); } else { - last = true; fname.clear(); } } running = true; if(!fname.empty()) { - int64_t size = File::getSize(fname); - int64_t sizeLeft = size; -#ifdef _WIN32 - if(buf == NULL) { - virtualBuf = true; - buf = (uint8_t*)VirtualAlloc(NULL, 2*BUF_SIZE, MEM_COMMIT, PAGE_READWRITE); - } -#endif - if(buf == NULL) { - virtualBuf = false; - buf = new uint8_t[BUF_SIZE]; - } try { + auto start = GET_TICK(); + File f(fname, File::READ, File::OPEN); - int64_t bs = max(TigerTree::calcBlockSize(f.getSize(), 10), MIN_BLOCK_SIZE); - uint64_t start = GET_TICK(); - uint32_t timestamp = f.getLastModified(); - TigerTree slowTTH(bs); - TigerTree* tth = &slowTTH; + auto size = f.getSize(); + auto timestamp = f.getLastModified(); + + auto sizeLeft = size; + auto bs = max(TigerTree::calcBlockSize(size, 10), MIN_BLOCK_SIZE); + + TigerTree tt(bs); CRC32Filter crc32; SFVReader sfv(fname); @@ -814,58 +573,48 @@ if(sfv.hasCRC()) xcrc32 = &crc32; - size_t n = 0; - TigerTree fastTTH(bs); - tth = &fastTTH; -#ifdef _WIN32 - if(!virtualBuf || !BOOLSETTING(FAST_HASH) || !fastHash(fname, buf, fastTTH, size, xcrc32)) { -#else - if(!BOOLSETTING(FAST_HASH) || !fastHash(fname, 0, fastTTH, size, xcrc32)) { -#endif - tth = &slowTTH; - crc32 = CRC32Filter(); - uint64_t lastRead = GET_TICK(); - - do { - size_t bufSize = BUF_SIZE; - if(SETTING(MAX_HASH_SPEED)> 0) { - uint64_t now = GET_TICK(); - uint64_t minTime = n * 1000LL / (SETTING(MAX_HASH_SPEED) * 1024LL * 1024LL); - if(lastRead + minTime> now) { - Thread::sleep(minTime - (now - lastRead)); - } - lastRead = lastRead + minTime; - } else { - lastRead = GET_TICK(); - } - n = f.read(buf, bufSize); - tth->update(buf, n); - if(xcrc32) - (*xcrc32)(buf, n); - - { - Lock l(cs); - currentSize = max(static_cast<uint64_t>(currentSize - n), static_cast<uint64_t>(0)); - } - sizeLeft -= n; - - instantPause(); - }while (n> 0 && !stop); - } else { - sizeLeft = 0; - } + auto lastRead = GET_TICK(); + + FileReader fr; + + fr.read(fname, [&](const void* buf, size_t n) -> bool { + if(SETTING(MAX_HASH_SPEED)> 0) { + uint64_t now = GET_TICK(); + uint64_t minTime = n * 1000LL / (SETTING(MAX_HASH_SPEED) * 1024LL * 1024LL); + if(lastRead + minTime> now) { + Thread::sleep(minTime - (now - lastRead)); + } + lastRead = lastRead + minTime; + } else { + lastRead = GET_TICK(); + } + + tt.update(buf, n); + if(xcrc32) + (*xcrc32)(buf, n); + + { + Lock l(cs); + currentSize = max(static_cast<uint64_t>(currentSize - n), static_cast<uint64_t>(0)); + } + sizeLeft -= n; + + instantPause(); + return !stop; + }); f.close(); - tth->finalize(); + tt.finalize(); uint64_t end = GET_TICK(); int64_t speed = 0; - if(end> start) { - speed = size * _LL(1000) / (end - start); + if(end > start) { + speed = size * 1000 / (end - start); } + if(xcrc32 && xcrc32->getValue() != sfv.getCRC()) { LogManager::getInstance()->message(str(F_("%1% not shared; calculated CRC32 does not match the one found in SFV file.") % Util::addBrackets(fname))); } else { - HashManager::getInstance()->hashDone(fname, timestamp, *tth, speed, size); + HashManager::getInstance()->hashDone(fname, timestamp, tt, speed, size); } } catch(const FileException& e) { LogManager::getInstance()->message(str(F_("Error hashing %1%: %2%") % Util::addBrackets(fname) % e.getError())); @@ -877,16 +626,6 @@ currentSize = 0; } running = false; - if(buf != NULL && (last || stop)) { - if(virtualBuf) { -#ifdef _WIN32 - VirtualFree(buf, 0, MEM_RELEASE); -#endif - } else { - delete [] buf; - } - buf = NULL; - } } return 0; } === modified file 'dcpp/SettingsManager.cpp' --- dcpp/SettingsManager.cpp 2011-12-26 12:53:16 +0000 +++ dcpp/SettingsManager.cpp 2012-01-01 14:22:32 +0000 @@ -82,7 +82,7 @@ "BoldSystemLog", "AutoRefreshTime", "UseTLS", "AutoSearchLimit", "AltSortOrder", "AutoKickNoFavs", "PromptPassword", "SpyFrameIgnoreTthSearches", "DontDlAlreadyQueued", "MaxCommandLength", "AllowUntrustedHubs", "AllowUntrustedClients", - "TLSPort", "FastHash", "SortFavUsersFirst", "SegmentedDL", "FollowLinks", + "TLSPort", "SortFavUsersFirst", "SegmentedDL", "FollowLinks", "SendBloom", "OwnerDrawnMenus", "Coral", "SearchFilterShared", "FinishedDLOnlyFull", "ConfirmExit", "ConfirmHubClosing", "ConfirmHubRemoval", "ConfirmUserRemoval", "ConfirmItemRemoval", "ConfirmADLSRemoval", "SearchMerge", "ToolbarSize", "TabWidth", "TabStyle", @@ -283,7 +283,6 @@ setDefault(MAX_COMMAND_LENGTH, 16*1024*1024); setDefault(ALLOW_UNTRUSTED_HUBS, true); setDefault(ALLOW_UNTRUSTED_CLIENTS, true); - setDefault(FAST_HASH, true); setDefault(SORT_FAVUSERS_FIRST, false); setDefault(SEGMENTED_DL, true); setDefault(FOLLOW_LINKS, false); === modified file 'dcpp/SettingsManager.h' --- dcpp/SettingsManager.h 2011-12-03 21:53:57 +0000 +++ dcpp/SettingsManager.h 2012-01-01 14:22:32 +0000 @@ -100,7 +100,7 @@ BOLD_SYSTEM_LOG, AUTO_REFRESH_TIME, USE_TLS, AUTO_SEARCH_LIMIT, ALT_SORT_ORDER, AUTO_KICK_NO_FAVS, PROMPT_PASSWORD, SPY_FRAME_IGNORE_TTH_SEARCHES, DONT_DL_ALREADY_QUEUED, MAX_COMMAND_LENGTH, ALLOW_UNTRUSTED_HUBS, ALLOW_UNTRUSTED_CLIENTS, - TLS_PORT, FAST_HASH, SORT_FAVUSERS_FIRST, SEGMENTED_DL, FOLLOW_LINKS, + TLS_PORT, SORT_FAVUSERS_FIRST, SEGMENTED_DL, FOLLOW_LINKS, SEND_BLOOM, OWNER_DRAWN_MENUS, CORAL, SEARCH_FILTER_SHARED, FINISHED_DL_ONLY_FULL, CONFIRM_EXIT, CONFIRM_HUB_CLOSING, CONFIRM_HUB_REMOVAL, CONFIRM_USER_REMOVAL, CONFIRM_ITEM_REMOVAL, CONFIRM_ADLS_REMOVAL, SEARCH_MERGE, TOOLBAR_SIZE, TAB_WIDTH, TAB_STYLE, === modified file 'utils/xsum.cpp' --- utils/xsum.cpp 2011-12-31 15:50:55 +0000 +++ utils/xsum.cpp 2012-01-01 14:22:32 +0000 @@ -34,12 +34,14 @@ TigerTree tt; size_t total = 0; - fr.read(x, [&](void* x, size_t n) { + fr.read(x, [&](const void* x, size_t n) -> bool { tt.update(x, n); total += n; if(total % (1024*1024) == 0) { std::cout << "."; } + + return true; }); cout << endl << Encoder::toBase32(tt.finalize(), TigerTree::BYTES); === modified file 'win32/AdvancedPage.cpp' --- win32/AdvancedPage.cpp 2011-05-04 19:32:00 +0000 +++ win32/AdvancedPage.cpp 2012-01-01 14:22:32 +0000 @@ -49,7 +49,6 @@ { SettingsManager::OWNER_DRAWN_MENUS, N_("Use extended menus with icons and titles"), IDH_SETTINGS_ADVANCED_OWNER_DRAWN_MENUS }, { SettingsManager::CORAL, N_("Use Coral network for HTTP downloads (improves reliability)"), IDH_SETTINGS_ADVANCED_CORAL }, { SettingsManager::SEGMENTED_DL, N_("Enable segmented downloads"), IDH_SETTINGS_ADVANCED_SEGMENTED_DL }, - { SettingsManager::FAST_HASH, N_("Use fast hashing method (disable if you have problems with hashing)"), IDH_SETTINGS_ADVANCED_FAST_HASH }, { 0, 0 } };
_______________________________________________ Mailing list: https://launchpad.net/~linuxdcpp-team Post to : linuxdcpp-team@lists.launchpad.net Unsubscribe : https://launchpad.net/~linuxdcpp-team More help : https://help.launchpad.net/ListHelp