From ae4a3cfa030438ca05ad3bf61fa301dee6c1dbb5 Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Thu, 11 Feb 2016 16:14:42 +0100 Subject: Move addPermRoot into Store --- src/libstore/store-api.hh | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) (limited to 'src/libstore/store-api.hh') diff --git a/src/libstore/store-api.hh b/src/libstore/store-api.hh index 1e43064394df..888ef3e2a083 100644 --- a/src/libstore/store-api.hh +++ b/src/libstore/store-api.hh @@ -255,6 +255,10 @@ public: `path' has disappeared. */ virtual void addIndirectRoot(const Path & path) = 0; + /* Register a permanent GC root. */ + Path addPermRoot(const Path & storePath, + const Path & gcRoot, bool indirect, bool allowOutsideRootsDir = false); + /* Acquire the global GC lock, then immediately release it. This function must be called after registering a new permanent root, but before exiting. Otherwise, it is possible that a running @@ -406,11 +410,6 @@ Path computeStorePathForText(const string & name, const string & s, void removeTempRoots(); -/* Register a permanent GC root. */ -Path addPermRoot(ref store, const Path & storePath, - const Path & gcRoot, bool indirect, bool allowOutsideRootsDir = false); - - /* Factory method: open the Nix database, either through the local or remote implementation. */ ref openStore(bool reserveSpace = true); -- cgit 1.4.1 From d0893725651a7657eab21ec4aad97146d2294c98 Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Mon, 15 Feb 2016 12:49:01 +0100 Subject: Add function to extract hash part of a store path --- src/libstore/local-store.cc | 2 +- src/libstore/store-api.cc | 9 ++++++++- src/libstore/store-api.hh | 6 ++++++ 3 files changed, 15 insertions(+), 2 deletions(-) (limited to 'src/libstore/store-api.hh') diff --git a/src/libstore/local-store.cc b/src/libstore/local-store.cc index 13179459f1c1..7c5945b2aa7e 100644 --- a/src/libstore/local-store.cc +++ b/src/libstore/local-store.cc @@ -1064,7 +1064,7 @@ StringSet LocalStore::queryDerivationOutputNames(const Path & path) Path LocalStore::queryPathFromHashPart(const string & hashPart) { - if (hashPart.size() != 32) throw Error("invalid hash part"); + if (hashPart.size() != storePathHashLen) throw Error("invalid hash part"); Path prefix = settings.nixStore + "/" + hashPart; diff --git a/src/libstore/store-api.cc b/src/libstore/store-api.cc index 039d07e29b3e..2f44401827f0 100644 --- a/src/libstore/store-api.cc +++ b/src/libstore/store-api.cc @@ -61,7 +61,14 @@ Path followLinksToStorePath(const Path & path) string storePathToName(const Path & path) { assertStorePath(path); - return string(path, settings.nixStore.size() + 34); + return string(path, settings.nixStore.size() + storePathHashLen + 2); +} + + +string storePathToHash(const Path & path) +{ + assertStorePath(path); + return string(path, settings.nixStore.size() + 1, storePathHashLen); } diff --git a/src/libstore/store-api.hh b/src/libstore/store-api.hh index 888ef3e2a083..f6fb6c8347a4 100644 --- a/src/libstore/store-api.hh +++ b/src/libstore/store-api.hh @@ -339,6 +339,9 @@ public: }; +const size_t storePathHashLen = 32; // base-32 characters, i.e. 160 bits + + /* !!! These should be part of the store API, I guess. */ /* Throw an exception if `path' is not directly in the Nix store. */ @@ -350,6 +353,9 @@ bool isStorePath(const Path & path); /* Extract the name part of the given store path. */ string storePathToName(const Path & path); +/* Extract the hash part of the given store path. */ +string storePathToHash(const Path & path); + void checkStoreName(const string & name); -- cgit 1.4.1 From c8f4d89a345cc06b64b0137e15567ec41c00881c Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Mon, 15 Feb 2016 14:48:38 +0100 Subject: Expose the export magic value and move LocalStore::queryReferences to Store --- src/libstore/local-store.cc | 17 +++-------------- src/libstore/local-store.hh | 2 -- src/libstore/store-api.cc | 7 +++++++ src/libstore/store-api.hh | 15 +++++++++------ 4 files changed, 19 insertions(+), 22 deletions(-) (limited to 'src/libstore/store-api.hh') diff --git a/src/libstore/local-store.cc b/src/libstore/local-store.cc index 7c5945b2aa7e..0b84e7027757 100644 --- a/src/libstore/local-store.cc +++ b/src/libstore/local-store.cc @@ -953,14 +953,6 @@ PathSet LocalStore::queryAllValidPaths() } -void LocalStore::queryReferences(const Path & path, - PathSet & references) -{ - ValidPathInfo info = queryPathInfo(path); - references.insert(info.references.begin(), info.references.end()); -} - - void LocalStore::queryReferrers_(const Path & path, PathSet & referrers) { SQLiteStmtUse use(stmtQueryReferrers); @@ -1493,9 +1485,6 @@ struct HashAndWriteSink : Sink }; -#define EXPORT_MAGIC 0x4558494e - - static void checkSecrecy(const Path & path) { struct stat st; @@ -1532,7 +1521,7 @@ void LocalStore::exportPath(const Path & path, bool sign, PathSet references; queryReferences(path, references); - hashAndWriteSink << EXPORT_MAGIC << path << references << queryDeriver(path); + hashAndWriteSink << exportMagic << path << references << queryDeriver(path); if (sign) { Hash hash = hashAndWriteSink.currentHash(); @@ -1608,8 +1597,8 @@ Path LocalStore::importPath(bool requireSignature, Source & source) restorePath(unpacked, hashAndReadSource); - unsigned int magic = readInt(hashAndReadSource); - if (magic != EXPORT_MAGIC) + uint32_t magic = readInt(hashAndReadSource); + if (magic != exportMagic) throw Error("Nix archive cannot be imported; wrong format"); Path dstPath = readStorePath(hashAndReadSource); diff --git a/src/libstore/local-store.hh b/src/libstore/local-store.hh index b6d39d345c94..a96000d9fbeb 100644 --- a/src/libstore/local-store.hh +++ b/src/libstore/local-store.hh @@ -108,8 +108,6 @@ public: Hash queryPathHash(const Path & path) override; - void queryReferences(const Path & path, PathSet & references) override; - void queryReferrers(const Path & path, PathSet & referrers) override; Path queryDeriver(const Path & path) override; diff --git a/src/libstore/store-api.cc b/src/libstore/store-api.cc index 2f44401827f0..54b6c2b5cc52 100644 --- a/src/libstore/store-api.cc +++ b/src/libstore/store-api.cc @@ -224,6 +224,13 @@ Path computeStorePathForText(const string & name, const string & s, } +void Store::queryReferences(const Path & path, PathSet & references) +{ + ValidPathInfo info = queryPathInfo(path); + references.insert(info.references.begin(), info.references.end()); +} + + /* Return a string accepted by decodeValidPathInfo() that registers the specified paths as valid. Note: it's the responsibility of the caller to provide a closure. */ diff --git a/src/libstore/store-api.hh b/src/libstore/store-api.hh index f6fb6c8347a4..54029bc13efb 100644 --- a/src/libstore/store-api.hh +++ b/src/libstore/store-api.hh @@ -12,6 +12,13 @@ namespace nix { +/* Size of the hash part of store paths, in base-32 characters. */ +const size_t storePathHashLen = 32; // i.e. 160 bits + +/* Magic header of exportPath() output. */ +const uint32_t exportMagic = 0x4558494e; + + typedef std::map Roots; @@ -156,10 +163,9 @@ public: /* Query the hash of a valid path. */ virtual Hash queryPathHash(const Path & path) = 0; - /* Query the set of outgoing FS references for a store path. The + /* Query the set of outgoing FS references for a store path. The result is not cleared. */ - virtual void queryReferences(const Path & path, - PathSet & references) = 0; + virtual void queryReferences(const Path & path, PathSet & references); /* Queries the set of incoming FS references for a store path. The result is not cleared. */ @@ -339,9 +345,6 @@ public: }; -const size_t storePathHashLen = 32; // base-32 characters, i.e. 160 bits - - /* !!! These should be part of the store API, I guess. */ /* Throw an exception if `path' is not directly in the Nix store. */ -- cgit 1.4.1 From 5ac27053e9bc4722dde5bd3243488d8e9a0b4623 Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Tue, 16 Feb 2016 11:49:12 +0100 Subject: Rename ValidPathInfo::hash -> narHash for consistency --- perl/lib/Nix/Store.xs | 2 +- src/libstore/build.cc | 6 +++--- src/libstore/local-store.cc | 32 ++++++++++++++++---------------- src/libstore/remote-store.cc | 2 +- src/libstore/store-api.cc | 4 ++-- src/libstore/store-api.hh | 4 ++-- src/nix-daemon/nix-daemon.cc | 2 +- src/nix-store/nix-store.cc | 12 ++++++------ 8 files changed, 32 insertions(+), 32 deletions(-) (limited to 'src/libstore/store-api.hh') diff --git a/perl/lib/Nix/Store.xs b/perl/lib/Nix/Store.xs index 78cc436ca255..beac53ebf42f 100644 --- a/perl/lib/Nix/Store.xs +++ b/perl/lib/Nix/Store.xs @@ -108,7 +108,7 @@ SV * queryPathInfo(char * path, int base32) XPUSHs(&PL_sv_undef); else XPUSHs(sv_2mortal(newSVpv(info.deriver.c_str(), 0))); - string s = "sha256:" + (base32 ? printHash32(info.hash) : printHash(info.hash)); + string s = "sha256:" + (base32 ? printHash32(info.narHash) : printHash(info.narHash)); XPUSHs(sv_2mortal(newSVpv(s.c_str(), 0))); mXPUSHi(info.registrationTime); mXPUSHi(info.narSize); diff --git a/src/libstore/build.cc b/src/libstore/build.cc index 180a558dc2e9..249ab2335bdf 100644 --- a/src/libstore/build.cc +++ b/src/libstore/build.cc @@ -2735,7 +2735,7 @@ void DerivationGoal::registerOutputs() if (buildMode == bmCheck) { if (!worker.store.isValidPath(path)) continue; ValidPathInfo info = worker.store.queryPathInfo(path); - if (hash.first != info.hash) { + if (hash.first != info.narHash) { if (settings.keepFailed) { Path dst = path + checkSuffix; if (pathExists(dst)) deletePath(dst); @@ -2799,7 +2799,7 @@ void DerivationGoal::registerOutputs() ValidPathInfo info; info.path = path; - info.hash = hash.first; + info.narHash = hash.first; info.narSize = hash.second; info.references = references; info.deriver = drvPath; @@ -3369,7 +3369,7 @@ void SubstitutionGoal::finished() ValidPathInfo info2; info2.path = storePath; - info2.hash = hash.first; + info2.narHash = hash.first; info2.narSize = hash.second; info2.references = info.references; info2.deriver = info.deriver; diff --git a/src/libstore/local-store.cc b/src/libstore/local-store.cc index 0b84e7027757..b2908877ce34 100644 --- a/src/libstore/local-store.cc +++ b/src/libstore/local-store.cc @@ -694,7 +694,7 @@ unsigned long long LocalStore::addValidPath(const ValidPathInfo & info, bool che { SQLiteStmtUse use(stmtRegisterValidPath); stmtRegisterValidPath.bind(info.path); - stmtRegisterValidPath.bind("sha256:" + printHash(info.hash)); + stmtRegisterValidPath.bind("sha256:" + printHash(info.narHash)); stmtRegisterValidPath.bind(info.registrationTime == 0 ? time(0) : info.registrationTime); if (info.deriver != "") stmtRegisterValidPath.bind(info.deriver); @@ -845,7 +845,7 @@ ValidPathInfo LocalStore::queryPathInfo(const Path & path) const char * s = (const char *) sqlite3_column_text(stmtQueryPathInfo, 1); assert(s); - info.hash = parseHashField(path, s); + info.narHash = parseHashField(path, s); info.registrationTime = sqlite3_column_int(stmtQueryPathInfo, 2); @@ -883,7 +883,7 @@ void LocalStore::updatePathInfo(const ValidPathInfo & info) stmtUpdatePathInfo.bind64(info.narSize); else stmtUpdatePathInfo.bind(); // null - stmtUpdatePathInfo.bind("sha256:" + printHash(info.hash)); + stmtUpdatePathInfo.bind("sha256:" + printHash(info.narHash)); stmtUpdatePathInfo.bind(info.path); if (sqlite3_step(stmtUpdatePathInfo) != SQLITE_DONE) throwSQLiteError(db, format("updating info of path ‘%1%’ in database") % info.path); @@ -1274,7 +1274,7 @@ void LocalStore::querySubstitutablePathInfos(const PathSet & paths, Hash LocalStore::queryPathHash(const Path & path) { - return queryPathInfo(path).hash; + return queryPathInfo(path).narHash; } @@ -1298,7 +1298,7 @@ void LocalStore::registerValidPaths(const ValidPathInfos & infos) PathSet paths; for (auto & i : infos) { - assert(i.hash.type == htSHA256); + assert(i.narHash.type == htSHA256); if (isValidPath_(i.path)) updatePathInfo(i); else @@ -1397,7 +1397,7 @@ Path LocalStore::addToStoreFromDump(const string & dump, const string & name, ValidPathInfo info; info.path = dstPath; - info.hash = hash.first; + info.narHash = hash.first; info.narSize = hash.second; registerValidPath(info); } @@ -1453,7 +1453,7 @@ Path LocalStore::addTextToStore(const string & name, const string & s, ValidPathInfo info; info.path = dstPath; - info.hash = hash.first; + info.narHash = hash.first; info.narSize = hash.second; info.references = references; registerValidPath(info); @@ -1680,7 +1680,7 @@ Path LocalStore::importPath(bool requireSignature, Source & source) ValidPathInfo info; info.path = dstPath; - info.hash = hash.first; + info.narHash = hash.first; info.narSize = hash.second; info.references = references; info.deriver = deriver != "" && isValidPath(deriver) ? deriver : ""; @@ -1764,21 +1764,21 @@ bool LocalStore::verifyStore(bool checkContents, bool repair) /* Check the content hash (optionally - slow). */ printMsg(lvlTalkative, format("checking contents of ‘%1%’") % i); - HashResult current = hashPath(info.hash.type, i); + HashResult current = hashPath(info.narHash.type, i); - if (info.hash != nullHash && info.hash != current.first) { + if (info.narHash != nullHash && info.narHash != current.first) { printMsg(lvlError, format("path ‘%1%’ was modified! " "expected hash ‘%2%’, got ‘%3%’") - % i % printHash(info.hash) % printHash(current.first)); + % i % printHash(info.narHash) % printHash(current.first)); if (repair) repairPath(i); else errors = true; } else { bool update = false; /* Fill in missing hashes. */ - if (info.hash == nullHash) { + if (info.narHash == nullHash) { printMsg(lvlError, format("fixing missing hash on ‘%1%’") % i); - info.hash = current.first; + info.narHash = current.first; update = true; } @@ -1867,9 +1867,9 @@ bool LocalStore::pathContentsGood(const Path & path) if (!pathExists(path)) res = false; else { - HashResult current = hashPath(info.hash.type, path); + HashResult current = hashPath(info.narHash.type, path); Hash nullHash(htSHA256); - res = info.hash == nullHash || info.hash == current.first; + res = info.narHash == nullHash || info.narHash == current.first; } pathContentsGoodCache[path] = res; if (!res) printMsg(lvlError, format("path ‘%1%’ is corrupted or missing!") % path); @@ -1921,7 +1921,7 @@ ValidPathInfo LocalStore::queryPathInfoOld(const Path & path) } else if (name == "Deriver") { res.deriver = value; } else if (name == "Hash") { - res.hash = parseHashField(path, value); + res.narHash = parseHashField(path, value); } else if (name == "Registered-At") { int n = 0; string2Int(value, n); diff --git a/src/libstore/remote-store.cc b/src/libstore/remote-store.cc index 679210d4cb16..ab2ebb9aecc3 100644 --- a/src/libstore/remote-store.cc +++ b/src/libstore/remote-store.cc @@ -273,7 +273,7 @@ ValidPathInfo RemoteStore::queryPathInfo(const Path & path) info.path = path; info.deriver = readString(from); if (info.deriver != "") assertStorePath(info.deriver); - info.hash = parseHash(htSHA256, readString(from)); + info.narHash = parseHash(htSHA256, readString(from)); info.references = readStorePaths(from); info.registrationTime = readInt(from); info.narSize = readLongLong(from); diff --git a/src/libstore/store-api.cc b/src/libstore/store-api.cc index 54b6c2b5cc52..82872cc335e3 100644 --- a/src/libstore/store-api.cc +++ b/src/libstore/store-api.cc @@ -245,7 +245,7 @@ string Store::makeValidityRegistration(const PathSet & paths, ValidPathInfo info = queryPathInfo(i); if (showHash) { - s += printHash(info.hash) + "\n"; + s += printHash(info.narHash) + "\n"; s += (format("%1%\n") % info.narSize).str(); } @@ -270,7 +270,7 @@ ValidPathInfo decodeValidPathInfo(std::istream & str, bool hashGiven) if (hashGiven) { string s; getline(str, s); - info.hash = parseHash(htSHA256, s); + info.narHash = parseHash(htSHA256, s); getline(str, s); if (!string2Int(s, info.narSize)) throw Error("number expected"); } diff --git a/src/libstore/store-api.hh b/src/libstore/store-api.hh index 54029bc13efb..6f50e3c55aba 100644 --- a/src/libstore/store-api.hh +++ b/src/libstore/store-api.hh @@ -92,7 +92,7 @@ struct ValidPathInfo { Path path; Path deriver; - Hash hash; + Hash narHash; PathSet references; time_t registrationTime = 0; unsigned long long narSize = 0; // 0 = unknown @@ -102,7 +102,7 @@ struct ValidPathInfo { return path == i.path - && hash == i.hash + && narHash == i.narHash && references == i.references; } }; diff --git a/src/nix-daemon/nix-daemon.cc b/src/nix-daemon/nix-daemon.cc index aa20aa67daa8..27694eac1a8c 100644 --- a/src/nix-daemon/nix-daemon.cc +++ b/src/nix-daemon/nix-daemon.cc @@ -515,7 +515,7 @@ static void performOp(ref store, bool trusted, unsigned int clientVe startWork(); ValidPathInfo info = store->queryPathInfo(path); stopWork(); - to << info.deriver << printHash(info.hash) << info.references + to << info.deriver << printHash(info.narHash) << info.references << info.registrationTime << info.narSize; break; } diff --git a/src/nix-store/nix-store.cc b/src/nix-store/nix-store.cc index 2dfd9e7c7411..7ec61eb625b0 100644 --- a/src/nix-store/nix-store.cc +++ b/src/nix-store/nix-store.cc @@ -374,8 +374,8 @@ static void opQuery(Strings opFlags, Strings opArgs) for (auto & j : paths) { ValidPathInfo info = store->queryPathInfo(j); if (query == qHash) { - assert(info.hash.type == htSHA256); - cout << format("sha256:%1%\n") % printHash32(info.hash); + assert(info.narHash.type == htSHA256); + cout << format("sha256:%1%\n") % printHash32(info.narHash); } else if (query == qSize) cout << format("%1%\n") % info.narSize; } @@ -567,7 +567,7 @@ static void registerValidity(bool reregister, bool hashGiven, bool canonicalise) canonicalisePathMetaData(info.path, -1); if (!hashGiven) { HashResult hash = hashPath(htSHA256, info.path); - info.hash = hash.first; + info.narHash = hash.first; info.narSize = hash.second; } infos.push_back(info); @@ -783,11 +783,11 @@ static void opVerifyPath(Strings opFlags, Strings opArgs) Path path = followLinksToStorePath(i); printMsg(lvlTalkative, format("checking path ‘%1%’...") % path); ValidPathInfo info = store->queryPathInfo(path); - HashResult current = hashPath(info.hash.type, path); - if (current.first != info.hash) { + HashResult current = hashPath(info.narHash.type, path); + if (current.first != info.narHash) { printMsg(lvlError, format("path ‘%1%’ was modified! expected hash ‘%2%’, got ‘%3%’") - % path % printHash(info.hash) % printHash(current.first)); + % path % printHash(info.narHash) % printHash(current.first)); status = 1; } } -- cgit 1.4.1 From 263187a2ec0a2ddd16cfd8c54c55e3455c9cd987 Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Wed, 24 Feb 2016 14:48:16 +0100 Subject: Move BinaryCacheStore / LocalBinaryCacheStore from Hydra So you can now do: $ NIX_REMOTE=file:///tmp/binary-cache nix-store -qR /nix/store/... --- src/libstore/binary-cache-store.cc | 292 +++++++++++++++++++++++++++++++ src/libstore/binary-cache-store.hh | 170 ++++++++++++++++++ src/libstore/local-binary-cache-store.cc | 44 +++++ src/libstore/local-binary-cache-store.hh | 31 ++++ src/libstore/store-api.cc | 18 +- src/libstore/store-api.hh | 24 ++- src/libutil/lru-cache.hh | 84 +++++++++ 7 files changed, 658 insertions(+), 5 deletions(-) create mode 100644 src/libstore/binary-cache-store.cc create mode 100644 src/libstore/binary-cache-store.hh create mode 100644 src/libstore/local-binary-cache-store.cc create mode 100644 src/libstore/local-binary-cache-store.hh create mode 100644 src/libutil/lru-cache.hh (limited to 'src/libstore/store-api.hh') diff --git a/src/libstore/binary-cache-store.cc b/src/libstore/binary-cache-store.cc new file mode 100644 index 000000000000..6bea0e6c903d --- /dev/null +++ b/src/libstore/binary-cache-store.cc @@ -0,0 +1,292 @@ +#include "binary-cache-store.hh" +#include "sync.hh" + +#include "archive.hh" +#include "compression.hh" +#include "derivations.hh" +#include "globals.hh" +#include "nar-info.hh" +#include "worker-protocol.hh" + +#include + +namespace nix { + +BinaryCacheStore::BinaryCacheStore(std::shared_ptr localStore, + const Path & secretKeyFile, const Path & publicKeyFile) + : localStore(localStore) +{ + if (secretKeyFile != "") + secretKey = std::unique_ptr(new SecretKey(readFile(secretKeyFile))); + + if (publicKeyFile != "") { + publicKeys = std::unique_ptr(new PublicKeys); + auto key = PublicKey(readFile(publicKeyFile)); + publicKeys->emplace(key.name, key); + } +} + +void BinaryCacheStore::init() +{ + std::string cacheInfoFile = "nix-cache-info"; + if (!fileExists(cacheInfoFile)) + upsertFile(cacheInfoFile, "StoreDir: " + settings.nixStore + "\n"); +} + +const BinaryCacheStore::Stats & BinaryCacheStore::getStats() +{ + return stats; +} + +Path BinaryCacheStore::narInfoFileFor(const Path & storePath) +{ + assertStorePath(storePath); + return storePathToHash(storePath) + ".narinfo"; +} + +void BinaryCacheStore::addToCache(const ValidPathInfo & info, + const string & nar) +{ + auto narInfoFile = narInfoFileFor(info.path); + if (fileExists(narInfoFile)) return; + + auto narInfo = make_ref(info); + + narInfo->narSize = nar.size(); + narInfo->narHash = hashString(htSHA256, nar); + + if (info.narHash.type != htUnknown && info.narHash != narInfo->narHash) + throw Error(format("refusing to copy corrupted path ‘%1%’ to binary cache") % info.path); + + /* Compress the NAR. */ + narInfo->compression = "xz"; + auto now1 = std::chrono::steady_clock::now(); + string narXz = compressXZ(nar); + auto now2 = std::chrono::steady_clock::now(); + narInfo->fileHash = hashString(htSHA256, narXz); + narInfo->fileSize = narXz.size(); + + auto duration = std::chrono::duration_cast(now2 - now1).count(); + printMsg(lvlTalkative, format("copying path ‘%1%’ (%2% bytes, compressed %3$.1f%% in %4% ms) to binary cache") + % narInfo->path % narInfo->narSize + % ((1.0 - (double) narXz.size() / nar.size()) * 100.0) + % duration); + + /* Atomically write the NAR file. */ + narInfo->url = "nar/" + printHash32(narInfo->fileHash) + ".nar.xz"; + if (!fileExists(narInfo->url)) { + stats.narWrite++; + upsertFile(narInfo->url, narXz); + } else + stats.narWriteAverted++; + + stats.narWriteBytes += nar.size(); + stats.narWriteCompressedBytes += narXz.size(); + stats.narWriteCompressionTimeMs += duration; + + /* Atomically write the NAR info file.*/ + if (secretKey) narInfo->sign(*secretKey); + + upsertFile(narInfoFile, narInfo->to_string()); + + { + auto state_(state.lock()); + state_->narInfoCache.upsert(narInfo->path, narInfo); + stats.narInfoCacheSize = state_->narInfoCache.size(); + } + + stats.narInfoWrite++; +} + +NarInfo BinaryCacheStore::readNarInfo(const Path & storePath) +{ + { + auto state_(state.lock()); + auto res = state_->narInfoCache.get(storePath); + if (res) { + stats.narInfoReadAverted++; + return **res; + } + } + + auto narInfoFile = narInfoFileFor(storePath); + auto narInfo = make_ref(getFile(narInfoFile), narInfoFile); + assert(narInfo->path == storePath); + + stats.narInfoRead++; + + if (publicKeys) { + if (!narInfo->checkSignature(*publicKeys)) + throw Error(format("invalid signature on NAR info file ‘%1%’") % narInfoFile); + } + + { + auto state_(state.lock()); + state_->narInfoCache.upsert(storePath, narInfo); + stats.narInfoCacheSize = state_->narInfoCache.size(); + } + + return *narInfo; +} + +bool BinaryCacheStore::isValidPath(const Path & storePath) +{ + return fileExists(narInfoFileFor(storePath)); +} + +void BinaryCacheStore::exportPath(const Path & storePath, bool sign, Sink & sink) +{ + assert(!sign); + + auto res = readNarInfo(storePath); + + auto nar = getFile(res.url); + + stats.narRead++; + stats.narReadCompressedBytes += nar.size(); + + /* Decompress the NAR. FIXME: would be nice to have the remote + side do this. */ + if (res.compression == "none") + ; + else if (res.compression == "xz") + nar = decompressXZ(nar); + else + throw Error(format("unknown NAR compression type ‘%1%’") % nar); + + stats.narReadBytes += nar.size(); + + printMsg(lvlTalkative, format("exporting path ‘%1%’ (%2% bytes)") % storePath % nar.size()); + + assert(nar.size() % 8 == 0); + + sink((unsigned char *) nar.c_str(), nar.size()); + + // FIXME: check integrity of NAR. + + sink << exportMagic << storePath << res.references << res.deriver << 0; +} + +Paths BinaryCacheStore::importPaths(bool requireSignature, Source & source) +{ + assert(!requireSignature); + Paths res; + while (true) { + unsigned long long n = readLongLong(source); + if (n == 0) break; + if (n != 1) throw Error("input doesn't look like something created by ‘nix-store --export’"); + res.push_back(importPath(source)); + } + return res; +} + +struct TeeSource : Source +{ + Source & readSource; + std::string data; + TeeSource(Source & readSource) : readSource(readSource) + { + } + size_t read(unsigned char * data, size_t len) + { + size_t n = readSource.read(data, len); + this->data.append((char *) data, n); + return n; + } +}; + +struct NopSink : ParseSink +{ +}; + +Path BinaryCacheStore::importPath(Source & source) +{ + /* FIXME: some cut&paste of LocalStore::importPath(). */ + + /* Extract the NAR from the source. */ + TeeSource tee(source); + NopSink sink; + parseDump(sink, tee); + + uint32_t magic = readInt(source); + if (magic != exportMagic) + throw Error("Nix archive cannot be imported; wrong format"); + + ValidPathInfo info; + info.path = readStorePath(source); + + info.references = readStorePaths(source); + + readString(source); // deriver, don't care + + bool haveSignature = readInt(source) == 1; + assert(!haveSignature); + + addToCache(info, tee.data); + + return info.path; +} + +ValidPathInfo BinaryCacheStore::queryPathInfo(const Path & storePath) +{ + return ValidPathInfo(readNarInfo(storePath)); +} + +void BinaryCacheStore::querySubstitutablePathInfos(const PathSet & paths, + SubstitutablePathInfos & infos) +{ + PathSet left; + + if (!localStore) return; + + for (auto & storePath : paths) { + if (!localStore->isValidPath(storePath)) { + left.insert(storePath); + continue; + } + ValidPathInfo info = localStore->queryPathInfo(storePath); + SubstitutablePathInfo sub; + sub.references = info.references; + sub.downloadSize = 0; + sub.narSize = info.narSize; + infos.emplace(storePath, sub); + } + + if (settings.useSubstitutes) + localStore->querySubstitutablePathInfos(left, infos); +} + +void BinaryCacheStore::buildPaths(const PathSet & paths, BuildMode buildMode) +{ + for (auto & storePath : paths) { + assert(!isDerivation(storePath)); + + if (isValidPath(storePath)) continue; + + if (!localStore) + throw Error(format("don't know how to realise path ‘%1%’ in a binary cache") % storePath); + + localStore->addTempRoot(storePath); + + if (!localStore->isValidPath(storePath)) + localStore->ensurePath(storePath); + + ValidPathInfo info = localStore->queryPathInfo(storePath); + + for (auto & ref : info.references) + if (ref != storePath) + ensurePath(ref); + + StringSink sink; + dumpPath(storePath, sink); + + addToCache(info, sink.s); + } +} + +void BinaryCacheStore::ensurePath(const Path & path) +{ + buildPaths({path}); +} + +} diff --git a/src/libstore/binary-cache-store.hh b/src/libstore/binary-cache-store.hh new file mode 100644 index 000000000000..d02f46dea7de --- /dev/null +++ b/src/libstore/binary-cache-store.hh @@ -0,0 +1,170 @@ +#pragma once + +#include "crypto.hh" +#include "store-api.hh" + +#include "lru-cache.hh" +#include "sync.hh" +#include "pool.hh" + +#include + +namespace nix { + +struct NarInfo; + +class BinaryCacheStore : public Store +{ +private: + + std::unique_ptr secretKey; + std::unique_ptr publicKeys; + + std::shared_ptr localStore; + + struct State + { + LRUCache> narInfoCache{32 * 1024}; + }; + + Sync state; + +protected: + + BinaryCacheStore(std::shared_ptr localStore, + const Path & secretKeyFile, const Path & publicKeyFile); + + virtual bool fileExists(const std::string & path) = 0; + + virtual void upsertFile(const std::string & path, const std::string & data) = 0; + + virtual std::string getFile(const std::string & path) = 0; + +public: + + virtual void init(); + + struct Stats + { + std::atomic narInfoRead{0}; + std::atomic narInfoReadAverted{0}; + std::atomic narInfoWrite{0}; + std::atomic narInfoCacheSize{0}; + std::atomic narRead{0}; + std::atomic narReadBytes{0}; + std::atomic narReadCompressedBytes{0}; + std::atomic narWrite{0}; + std::atomic narWriteAverted{0}; + std::atomic narWriteBytes{0}; + std::atomic narWriteCompressedBytes{0}; + std::atomic narWriteCompressionTimeMs{0}; + }; + + const Stats & getStats(); + +private: + + Stats stats; + + std::string narInfoFileFor(const Path & storePath); + + void addToCache(const ValidPathInfo & info, const string & nar); + +protected: + + NarInfo readNarInfo(const Path & storePath); + +public: + + bool isValidPath(const Path & path) override; + + PathSet queryValidPaths(const PathSet & paths) override + { abort(); } + + PathSet queryAllValidPaths() override + { abort(); } + + ValidPathInfo queryPathInfo(const Path & path) override; + + Hash queryPathHash(const Path & path) override + { abort(); } + + void queryReferrers(const Path & path, + PathSet & referrers) override + { abort(); } + + Path queryDeriver(const Path & path) override + { abort(); } + + PathSet queryValidDerivers(const Path & path) override + { abort(); } + + PathSet queryDerivationOutputs(const Path & path) override + { abort(); } + + StringSet queryDerivationOutputNames(const Path & path) override + { abort(); } + + Path queryPathFromHashPart(const string & hashPart) override + { abort(); } + + PathSet querySubstitutablePaths(const PathSet & paths) override + { abort(); } + + void querySubstitutablePathInfos(const PathSet & paths, + SubstitutablePathInfos & infos) override; + + Path addToStore(const string & name, const Path & srcPath, + bool recursive = true, HashType hashAlgo = htSHA256, + PathFilter & filter = defaultPathFilter, bool repair = false) override + { abort(); } + + Path addTextToStore(const string & name, const string & s, + const PathSet & references, bool repair = false) override + { abort(); } + + void exportPath(const Path & path, bool sign, + Sink & sink) override; + + Paths importPaths(bool requireSignature, Source & source) override; + + Path importPath(Source & source); + + void buildPaths(const PathSet & paths, BuildMode buildMode = bmNormal) override; + + BuildResult buildDerivation(const Path & drvPath, const BasicDerivation & drv, + BuildMode buildMode = bmNormal) override + { abort(); } + + void ensurePath(const Path & path) override; + + void addTempRoot(const Path & path) override + { abort(); } + + void addIndirectRoot(const Path & path) override + { abort(); } + + void syncWithGC() override + { } + + Roots findRoots() override + { abort(); } + + void collectGarbage(const GCOptions & options, GCResults & results) override + { abort(); } + + PathSet queryFailedPaths() override + { return PathSet(); } + + void clearFailedPaths(const PathSet & paths) override + { } + + void optimiseStore() override + { } + + bool verifyStore(bool checkContents, bool repair) override + { return true; } + +}; + +} diff --git a/src/libstore/local-binary-cache-store.cc b/src/libstore/local-binary-cache-store.cc new file mode 100644 index 000000000000..5714688e02df --- /dev/null +++ b/src/libstore/local-binary-cache-store.cc @@ -0,0 +1,44 @@ +#include "local-binary-cache-store.hh" + +namespace nix { + +LocalBinaryCacheStore::LocalBinaryCacheStore(std::shared_ptr localStore, + const Path & secretKeyFile, const Path & publicKeyFile, + const Path & binaryCacheDir) + : BinaryCacheStore(localStore, secretKeyFile, publicKeyFile) + , binaryCacheDir(binaryCacheDir) +{ +} + +void LocalBinaryCacheStore::init() +{ + createDirs(binaryCacheDir + "/nar"); + BinaryCacheStore::init(); +} + +static void atomicWrite(const Path & path, const std::string & s) +{ + Path tmp = path + ".tmp." + std::to_string(getpid()); + AutoDelete del(tmp, false); + writeFile(tmp, s); + if (rename(tmp.c_str(), path.c_str())) + throw SysError(format("renaming ‘%1%’ to ‘%2%’") % tmp % path); + del.cancel(); +} + +bool LocalBinaryCacheStore::fileExists(const std::string & path) +{ + return pathExists(binaryCacheDir + "/" + path); +} + +void LocalBinaryCacheStore::upsertFile(const std::string & path, const std::string & data) +{ + atomicWrite(binaryCacheDir + "/" + path, data); +} + +std::string LocalBinaryCacheStore::getFile(const std::string & path) +{ + return readFile(binaryCacheDir + "/" + path); +} + +} diff --git a/src/libstore/local-binary-cache-store.hh b/src/libstore/local-binary-cache-store.hh new file mode 100644 index 000000000000..0303ebe734a6 --- /dev/null +++ b/src/libstore/local-binary-cache-store.hh @@ -0,0 +1,31 @@ +#pragma once + +#include "binary-cache-store.hh" + +namespace nix { + +class LocalBinaryCacheStore : public BinaryCacheStore +{ +private: + + Path binaryCacheDir; + +public: + + LocalBinaryCacheStore(std::shared_ptr localStore, + const Path & secretKeyFile, const Path & publicKeyFile, + const Path & binaryCacheDir); + + void init() override; + +protected: + + bool fileExists(const std::string & path) override; + + void upsertFile(const std::string & path, const std::string & data) override; + + std::string getFile(const std::string & path) override; + +}; + +} diff --git a/src/libstore/store-api.cc b/src/libstore/store-api.cc index 82872cc335e3..f9c8767709e7 100644 --- a/src/libstore/store-api.cc +++ b/src/libstore/store-api.cc @@ -313,18 +313,24 @@ void Store::exportPaths(const Paths & paths, #include "local-store.hh" -#include "serialise.hh" #include "remote-store.hh" +#include "local-binary-cache-store.hh" namespace nix { -ref openStore(bool reserveSpace) +ref openStoreAt(const std::string & uri, bool reserveSpace) { + if (std::string(uri, 0, 7) == "file://") { + return make_ref(std::shared_ptr(0), + "", "", // FIXME: allow the signing key to be set + std::string(uri, 7)); + } + enum { mDaemon, mLocal, mAuto } mode; - mode = getEnv("NIX_REMOTE") == "daemon" ? mDaemon : mAuto; + mode = uri == "daemon" ? mDaemon : mAuto; if (mode == mAuto) { if (LocalStore::haveWriteAccess()) @@ -341,4 +347,10 @@ ref openStore(bool reserveSpace) } +ref openStore(bool reserveSpace) +{ + return openStoreAt(getEnv("NIX_REMOTE"), reserveSpace); +} + + } diff --git a/src/libstore/store-api.hh b/src/libstore/store-api.hh index 6f50e3c55aba..5fe42c973787 100644 --- a/src/libstore/store-api.hh +++ b/src/libstore/store-api.hh @@ -419,8 +419,28 @@ Path computeStorePathForText(const string & name, const string & s, void removeTempRoots(); -/* Factory method: open the Nix database, either through the local or - remote implementation. */ +/* Return a Store object to access the Nix store denoted by + ‘uri’ (slight misnomer...). Supported values are: + + * ‘direct’: The Nix store in /nix/store and database in + /nix/var/nix/db, accessed directly. + + * ‘daemon’: The Nix store accessed via a Unix domain socket + connection to nix-daemon. + + * ‘file://’: A binary cache stored in . + + If ‘uri’ is empty, it defaults to ‘direct’ or ‘daemon’ depending on + whether the user has write access to the local Nix store/database. + + The Boolean ‘reserveSpace’ denotes whether some disk space should + be reserved to enable future garbage collector runs. It should be + set to true *unless* you're going to collect garbage. +*/ +ref openStoreAt(const std::string & uri, bool reserveSpace = true); + + +/* Open the store indicated by the ‘NIX_REMOTE’ environment variable. */ ref openStore(bool reserveSpace = true); diff --git a/src/libutil/lru-cache.hh b/src/libutil/lru-cache.hh new file mode 100644 index 000000000000..3c6569cf41ca --- /dev/null +++ b/src/libutil/lru-cache.hh @@ -0,0 +1,84 @@ +#pragma once + +#include +#include + +namespace nix { + +/* A simple least-recently used cache. Not thread-safe. */ +template +class LRUCache +{ +private: + + size_t maxSize; + + // Stupid wrapper to get around circular dependency between Data + // and LRU. + struct LRUIterator; + + using Data = std::map>; + using LRU = std::list; + + struct LRUIterator { typename LRU::iterator it; }; + + Data data; + LRU lru; + +public: + + LRUCache(size_t maxSize) : maxSize(maxSize) { } + + /* Insert or upsert an item in the cache. */ + void upsert(const Key & key, const Value & value) + { + erase(key); + + if (data.size() >= maxSize) { + /* Retire the oldest item. */ + auto oldest = lru.begin(); + data.erase(*oldest); + lru.erase(oldest); + } + + auto res = data.emplace(key, std::make_pair(LRUIterator(), value)); + assert(res.second); + auto & i(res.first); + + auto j = lru.insert(lru.end(), i); + + i->second.first.it = j; + } + + bool erase(const Key & key) + { + auto i = data.find(key); + if (i == data.end()) return false; + lru.erase(i->second.first.it); + data.erase(i); + return true; + } + + /* Look up an item in the cache. If it's exists, it becomes the + most recently used item. */ + // FIXME: use boost::optional? + Value * get(const Key & key) + { + auto i = data.find(key); + if (i == data.end()) return 0; + + /* Move this item to the back of the LRU list. */ + lru.erase(i->second.first.it); + auto j = lru.insert(lru.end(), i); + i->second.first.it = j; + + return &i->second.second; + } + + size_t size() + { + return data.size(); + } +}; + +} -- cgit 1.4.1 From 28e7e29abdcdaf60ba8a5fad3738fe4a8f3db9a8 Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Wed, 24 Feb 2016 17:33:53 +0100 Subject: Eliminate reserveSpace flag --- src/libstore/gc.cc | 3 +++ src/libstore/local-store.cc | 30 +++++++++++--------------- src/libstore/local-store.hh | 4 +++- src/libstore/remote-store.cc | 4 ++-- src/libstore/remote-store.hh | 2 +- src/libstore/store-api.cc | 8 +++---- src/libstore/store-api.hh | 10 +++------ src/nix-collect-garbage/nix-collect-garbage.cc | 2 +- src/nix-daemon/nix-daemon.cc | 5 ++--- src/nix-store/nix-store.cc | 2 +- 10 files changed, 33 insertions(+), 37 deletions(-) (limited to 'src/libstore/store-api.hh') diff --git a/src/libstore/gc.cc b/src/libstore/gc.cc index d19af1cefaf2..1bc8d162f837 100644 --- a/src/libstore/gc.cc +++ b/src/libstore/gc.cc @@ -608,6 +608,9 @@ void LocalStore::collectGarbage(const GCOptions & options, GCResults & results) state.shouldDelete = options.action == GCOptions::gcDeleteDead || options.action == GCOptions::gcDeleteSpecific; + if (state.shouldDelete && pathExists(reservedPath)) + deletePath(reservedPath); + /* Acquire the global GC root. This prevents a) New roots from being added. b) Processes from creating new temporary root files. */ diff --git a/src/libstore/local-store.cc b/src/libstore/local-store.cc index 1a12c91c732d..da4d932df6f9 100644 --- a/src/libstore/local-store.cc +++ b/src/libstore/local-store.cc @@ -216,8 +216,9 @@ void checkStoreNotSymlink() } -LocalStore::LocalStore(bool reserveSpace) - : didSetSubstituterEnv(false) +LocalStore::LocalStore() + : reservedPath(settings.nixDBPath + "/reserved") + , didSetSubstituterEnv(false) { schemaPath = settings.nixDBPath + "/schema"; @@ -276,25 +277,20 @@ LocalStore::LocalStore(bool reserveSpace) needed, we reserve some dummy space that we can free just before doing a garbage collection. */ try { - Path reservedPath = settings.nixDBPath + "/reserved"; - if (reserveSpace) { - struct stat st; - if (stat(reservedPath.c_str(), &st) == -1 || - st.st_size != settings.reservedSize) - { - AutoCloseFD fd = open(reservedPath.c_str(), O_WRONLY | O_CREAT, 0600); - int res = -1; + struct stat st; + if (stat(reservedPath.c_str(), &st) == -1 || + st.st_size != settings.reservedSize) + { + AutoCloseFD fd = open(reservedPath.c_str(), O_WRONLY | O_CREAT, 0600); + int res = -1; #if HAVE_POSIX_FALLOCATE - res = posix_fallocate(fd, 0, settings.reservedSize); + res = posix_fallocate(fd, 0, settings.reservedSize); #endif - if (res == -1) { - writeFull(fd, string(settings.reservedSize, 'X')); - ftruncate(fd, settings.reservedSize); - } + if (res == -1) { + writeFull(fd, string(settings.reservedSize, 'X')); + ftruncate(fd, settings.reservedSize); } } - else - deletePath(reservedPath); } catch (SysError & e) { /* don't care about errors */ } diff --git a/src/libstore/local-store.hh b/src/libstore/local-store.hh index a96000d9fbeb..5582acd0f72f 100644 --- a/src/libstore/local-store.hh +++ b/src/libstore/local-store.hh @@ -88,11 +88,13 @@ private: Path linksDir; + Path reservedPath; + public: /* Initialise the local store, upgrading the schema if necessary. */ - LocalStore(bool reserveSpace = true); + LocalStore(); ~LocalStore(); diff --git a/src/libstore/remote-store.cc b/src/libstore/remote-store.cc index 2f540c640e0b..a1ac169c2b9e 100644 --- a/src/libstore/remote-store.cc +++ b/src/libstore/remote-store.cc @@ -49,7 +49,7 @@ RemoteStore::RemoteStore(size_t maxConnections) } -ref RemoteStore::openConnection(bool reserveSpace) +ref RemoteStore::openConnection() { auto conn = make_ref(); @@ -106,7 +106,7 @@ ref RemoteStore::openConnection(bool reserveSpace) } if (GET_PROTOCOL_MINOR(conn->daemonVersion) >= 11) - conn->to << reserveSpace; + conn->to << false; conn->processStderr(); } diff --git a/src/libstore/remote-store.hh b/src/libstore/remote-store.hh index af417b84ff9c..ddfb70a669a0 100644 --- a/src/libstore/remote-store.hh +++ b/src/libstore/remote-store.hh @@ -106,7 +106,7 @@ private: ref> connections; - ref openConnection(bool reserveSpace = true); + ref openConnection(); void setOptions(ref conn); }; diff --git a/src/libstore/store-api.cc b/src/libstore/store-api.cc index 9f72bbd83ea3..7058249f06da 100644 --- a/src/libstore/store-api.cc +++ b/src/libstore/store-api.cc @@ -320,7 +320,7 @@ void Store::exportPaths(const Paths & paths, namespace nix { -ref openStoreAt(const std::string & uri, bool reserveSpace) +ref openStoreAt(const std::string & uri) { if (std::string(uri, 0, 7) == "file://") { auto store = make_ref(std::shared_ptr(0), @@ -345,13 +345,13 @@ ref openStoreAt(const std::string & uri, bool reserveSpace) return mode == mDaemon ? (ref) make_ref() - : (ref) make_ref(reserveSpace); + : (ref) make_ref(); } -ref openStore(bool reserveSpace) +ref openStore() { - return openStoreAt(getEnv("NIX_REMOTE"), reserveSpace); + return openStoreAt(getEnv("NIX_REMOTE")); } diff --git a/src/libstore/store-api.hh b/src/libstore/store-api.hh index 5fe42c973787..84ede157e87a 100644 --- a/src/libstore/store-api.hh +++ b/src/libstore/store-api.hh @@ -432,16 +432,12 @@ void removeTempRoots(); If ‘uri’ is empty, it defaults to ‘direct’ or ‘daemon’ depending on whether the user has write access to the local Nix store/database. - - The Boolean ‘reserveSpace’ denotes whether some disk space should - be reserved to enable future garbage collector runs. It should be - set to true *unless* you're going to collect garbage. -*/ -ref openStoreAt(const std::string & uri, bool reserveSpace = true); + set to true *unless* you're going to collect garbage. */ +ref openStoreAt(const std::string & uri); /* Open the store indicated by the ‘NIX_REMOTE’ environment variable. */ -ref openStore(bool reserveSpace = true); +ref openStore(); /* Display a set of paths in human-readable form (i.e., between quotes diff --git a/src/nix-collect-garbage/nix-collect-garbage.cc b/src/nix-collect-garbage/nix-collect-garbage.cc index b9ccafb751c1..3aa348581b19 100644 --- a/src/nix-collect-garbage/nix-collect-garbage.cc +++ b/src/nix-collect-garbage/nix-collect-garbage.cc @@ -82,7 +82,7 @@ int main(int argc, char * * argv) // Run the actual garbage collector. if (!dryRun) { - auto store = openStore(false); + auto store = openStore(); options.action = GCOptions::gcDeleteDead; GCResults results; PrintFreed freed(true, results); diff --git a/src/nix-daemon/nix-daemon.cc b/src/nix-daemon/nix-daemon.cc index 27694eac1a8c..e3c12ea0e0ea 100644 --- a/src/nix-daemon/nix-daemon.cc +++ b/src/nix-daemon/nix-daemon.cc @@ -562,9 +562,8 @@ static void processConnection(bool trusted) if (GET_PROTOCOL_MINOR(clientVersion) >= 14 && readInt(from)) setAffinityTo(readInt(from)); - bool reserveSpace = true; if (GET_PROTOCOL_MINOR(clientVersion) >= 11) - reserveSpace = readInt(from) != 0; + readInt(from); // obsolete reserveSpace /* Send startup error messages to the client. */ startWork(); @@ -582,7 +581,7 @@ static void processConnection(bool trusted) #endif /* Open the store. */ - auto store = make_ref(reserveSpace); + auto store = make_ref(); stopWork(); to.flush(); diff --git a/src/nix-store/nix-store.cc b/src/nix-store/nix-store.cc index 7ec61eb625b0..48c2865e5ecc 100644 --- a/src/nix-store/nix-store.cc +++ b/src/nix-store/nix-store.cc @@ -1131,7 +1131,7 @@ int main(int argc, char * * argv) if (!op) throw UsageError("no operation specified"); if (op != opDump && op != opRestore) /* !!! hack */ - store = openStore(op != opGC); + store = openStore(); op(opFlags, opArgs); }); -- cgit 1.4.1