diff --git a/src/libfetchers/git-utils.cc b/src/libfetchers/git-utils.cc index cfb396bb416..d4a71738df4 100644 --- a/src/libfetchers/git-utils.cc +++ b/src/libfetchers/git-utils.cc @@ -359,7 +359,7 @@ struct GitRepoImpl : GitRepo, std::enable_shared_from_this ThreadPool pool; - auto process = [&done, &pool, &repoPool](this const auto & process, const git_oid & oid) -> void { + auto process = [&done, &pool, &repoPool](this auto const & process, const git_oid & oid) -> void { auto repo(repoPool.get()); auto _commit = lookupObject(*repo, oid, GIT_OBJECT_COMMIT); diff --git a/src/libmain/progress-bar.cc b/src/libmain/progress-bar.cc index d53e4f5a69d..e81f5913536 100644 --- a/src/libmain/progress-bar.cc +++ b/src/libmain/progress-bar.cc @@ -470,11 +470,7 @@ class ProgressBar : public Logger } } - auto width = getWindowSize().second; - if (width <= 0) - width = std::numeric_limits::max(); - - redraw("\r" + filterANSIEscapes(line, false, width) + ANSI_NORMAL + "\e[K"); + redraw("\r" + filterANSIEscapes(line, false, getWindowWidth()) + ANSI_NORMAL + "\e[K"); return nextWakeup; } diff --git a/src/libstore/active-builds.cc b/src/libstore/active-builds.cc new file mode 100644 index 00000000000..b271a1ba45f --- /dev/null +++ b/src/libstore/active-builds.cc @@ -0,0 +1,145 @@ +#include "nix/store/active-builds.hh" +#include "nix/util/json-utils.hh" + +#include + +#ifndef _WIN32 +# include +#endif + +namespace nix { + +UserInfo UserInfo::fromUid(uid_t uid) +{ + UserInfo info; + info.uid = uid; + +#ifndef _WIN32 + // Look up the user name for the UID (thread-safe) + struct passwd pwd; + struct passwd * result; + std::vector buf(16384); + if (getpwuid_r(uid, &pwd, buf.data(), buf.size(), &result) == 0 && result) + info.name = result->pw_name; +#endif + + return info; +} + +} // namespace nix + +namespace nlohmann { + +using namespace nix; + +UserInfo adl_serializer::from_json(const json & j) +{ + return UserInfo{ + .uid = j.at("uid").get(), + .name = j.contains("name") && !j.at("name").is_null() + ? std::optional(j.at("name").get()) + : std::nullopt, + }; +} + +void adl_serializer::to_json(json & j, const UserInfo & info) +{ + j = nlohmann::json{ + {"uid", info.uid}, + {"name", info.name}, + }; +} + +// Durations are serialized as floats representing seconds. +static std::optional parseDuration(const json & j, const char * key) +{ + if (j.contains(key) && !j.at(key).is_null()) + return std::chrono::duration_cast( + std::chrono::duration(j.at(key).get())); + else + return std::nullopt; +} + +static nlohmann::json printDuration(const std::optional & duration) +{ + return duration + ? nlohmann::json( + std::chrono::duration_cast>(*duration) + .count()) + : nullptr; +} + +ActiveBuildInfo::ProcessInfo adl_serializer::from_json(const json & j) +{ + return ActiveBuildInfo::ProcessInfo{ + .pid = j.at("pid").get(), + .parentPid = j.at("parentPid").get(), + .user = j.at("user").get(), + .argv = j.at("argv").get>(), + .utime = parseDuration(j, "utime"), + .stime = parseDuration(j, "stime"), + .cutime = parseDuration(j, "cutime"), + .cstime = parseDuration(j, "cstime"), + }; +} + +void adl_serializer::to_json(json & j, const ActiveBuildInfo::ProcessInfo & process) +{ + j = nlohmann::json{ + {"pid", process.pid}, + {"parentPid", process.parentPid}, + {"user", process.user}, + {"argv", process.argv}, + {"utime", printDuration(process.utime)}, + {"stime", printDuration(process.stime)}, + {"cutime", printDuration(process.cutime)}, + {"cstime", printDuration(process.cstime)}, + }; +} + +ActiveBuild adl_serializer::from_json(const json & j) +{ + return ActiveBuild{ + .nixPid = j.at("nixPid").get(), + .clientPid = j.at("clientPid").get>(), + .clientUid = j.at("clientUid").get>(), + .mainPid = j.at("mainPid").get(), + .mainUser = j.at("mainUser").get(), + .cgroup = j.at("cgroup").get>(), + .startTime = (time_t) j.at("startTime").get(), + .derivation = StorePath{getString(j.at("derivation"))}, + }; +} + +void adl_serializer::to_json(json & j, const ActiveBuild & build) +{ + j = nlohmann::json{ + {"nixPid", build.nixPid}, + {"clientPid", build.clientPid}, + {"clientUid", build.clientUid}, + {"mainPid", build.mainPid}, + {"mainUser", build.mainUser}, + {"cgroup", build.cgroup}, + {"startTime", (double) build.startTime}, + {"derivation", build.derivation.to_string()}, + }; +} + +ActiveBuildInfo adl_serializer::from_json(const json & j) +{ + ActiveBuildInfo info(adl_serializer::from_json(j)); + info.processes = j.at("processes").get>(); + info.utime = parseDuration(j, "utime"); + info.stime = parseDuration(j, "stime"); + return info; +} + +void adl_serializer::to_json(json & j, const ActiveBuildInfo & build) +{ + adl_serializer::to_json(j, build); + j["processes"] = build.processes; + j["utime"] = printDuration(build.utime); + j["stime"] = printDuration(build.stime); +} + +} // namespace nlohmann diff --git a/src/libstore/daemon.cc b/src/libstore/daemon.cc index 7df44158132..768dc93994c 100644 --- a/src/libstore/daemon.cc +++ b/src/libstore/daemon.cc @@ -17,6 +17,7 @@ #include "nix/util/git.hh" #include "nix/util/logging.hh" #include "nix/store/globals.hh" +#include "nix/store/active-builds.hh" #ifndef _WIN32 // TODO need graceful async exit support on Windows? # include "nix/util/monitor-fd.hh" @@ -1014,6 +1015,15 @@ static void performOp( case WorkerProto::Op::ClearFailedPaths: throw Error("Removed operation %1%", op); + case WorkerProto::Op::QueryActiveBuilds: { + logger->startWork(); + auto & activeBuildsStore = require(*store); + auto activeBuilds = activeBuildsStore.queryActiveBuilds(); + logger->stopWork(); + conn.to << nlohmann::json(activeBuilds).dump(); + break; + } + default: throw Error("invalid operation %1%", op); } diff --git a/src/libstore/include/nix/store/active-builds.hh b/src/libstore/include/nix/store/active-builds.hh new file mode 100644 index 00000000000..c8a40e13798 --- /dev/null +++ b/src/libstore/include/nix/store/active-builds.hh @@ -0,0 +1,108 @@ +#pragma once + +#include "nix/util/util.hh" +#include "nix/util/json-impls.hh" +#include "nix/store/path.hh" + +#include +#include + +namespace nix { + +/** + * A uid and optional corresponding user name. + */ +struct UserInfo +{ + uid_t uid = -1; + std::optional name; + + /** + * Create a UserInfo from a UID, looking up the username if possible. + */ + static UserInfo fromUid(uid_t uid); +}; + +struct ActiveBuild +{ + pid_t nixPid; + + std::optional clientPid; + std::optional clientUid; + + pid_t mainPid; + UserInfo mainUser; + std::optional cgroup; + + time_t startTime; + + StorePath derivation; +}; + +struct ActiveBuildInfo : ActiveBuild +{ + struct ProcessInfo + { + pid_t pid = 0; + pid_t parentPid = 0; + UserInfo user; + std::vector argv; + std::optional utime, stime, cutime, cstime; + }; + + // User/system CPU time for the entire cgroup, if available. + std::optional utime, stime; + + std::vector processes; +}; + +struct TrackActiveBuildsStore +{ + struct BuildHandle + { + TrackActiveBuildsStore & tracker; + uint64_t id; + + BuildHandle(TrackActiveBuildsStore & tracker, uint64_t id) + : tracker(tracker) + , id(id) + { + } + + BuildHandle(BuildHandle && other) noexcept + : tracker(other.tracker) + , id(other.id) + { + other.id = 0; + } + + ~BuildHandle() + { + if (id) { + try { + tracker.buildFinished(*this); + } catch (...) { + ignoreExceptionInDestructor(); + } + } + } + }; + + virtual BuildHandle buildStarted(const ActiveBuild & build) = 0; + + virtual void buildFinished(const BuildHandle & handle) = 0; +}; + +struct QueryActiveBuildsStore +{ + inline static std::string operationName = "Querying active builds"; + + virtual std::vector queryActiveBuilds() = 0; +}; + +} // namespace nix + +JSON_IMPL(UserInfo) +JSON_IMPL(ActiveBuild) +JSON_IMPL(ActiveBuildInfo) +JSON_IMPL(ActiveBuildInfo::ProcessInfo) diff --git a/src/libstore/include/nix/store/local-store.hh b/src/libstore/include/nix/store/local-store.hh index b871aaee2ce..0decabd5647 100644 --- a/src/libstore/include/nix/store/local-store.hh +++ b/src/libstore/include/nix/store/local-store.hh @@ -6,6 +6,7 @@ #include "nix/store/pathlocks.hh" #include "nix/store/store-api.hh" #include "nix/store/indirect-root-store.hh" +#include "nix/store/active-builds.hh" #include "nix/util/sync.hh" #include @@ -125,7 +126,10 @@ public: StoreReference getReference() const override; }; -class LocalStore : public virtual IndirectRootStore, public virtual GcStore +class LocalStore : public virtual IndirectRootStore, + public virtual GcStore, + public virtual TrackActiveBuildsStore, + public virtual QueryActiveBuildsStore { public: @@ -457,6 +461,24 @@ private: friend struct PathSubstitutionGoal; friend struct DerivationGoal; + +private: + + std::filesystem::path activeBuildsDir; + + struct ActiveBuildFile + { + AutoCloseFD fd; + AutoDelete del; + }; + + Sync> activeBuilds; + + std::vector queryActiveBuilds() override; + + BuildHandle buildStarted(const ActiveBuild & build) override; + + void buildFinished(const BuildHandle & handle) override; }; } // namespace nix diff --git a/src/libstore/include/nix/store/meson.build b/src/libstore/include/nix/store/meson.build index 8e88ec51f66..82d9ac85730 100644 --- a/src/libstore/include/nix/store/meson.build +++ b/src/libstore/include/nix/store/meson.build @@ -10,6 +10,7 @@ config_pub_h = configure_file( ) headers = [ config_pub_h ] + files( + 'active-builds.hh', 'async-path-writer.hh', 'aws-creds.hh', 'binary-cache-store.hh', diff --git a/src/libstore/include/nix/store/remote-store.hh b/src/libstore/include/nix/store/remote-store.hh index df9162d134c..6a207d24a91 100644 --- a/src/libstore/include/nix/store/remote-store.hh +++ b/src/libstore/include/nix/store/remote-store.hh @@ -7,6 +7,7 @@ #include "nix/store/store-api.hh" #include "nix/store/gc-store.hh" #include "nix/store/log-store.hh" +#include "nix/store/active-builds.hh" namespace nix { @@ -36,7 +37,10 @@ struct RemoteStoreConfig : virtual StoreConfig * \todo RemoteStore is a misnomer - should be something like * DaemonStore. */ -struct RemoteStore : public virtual Store, public virtual GcStore, public virtual LogStore +struct RemoteStore : public virtual Store, + public virtual GcStore, + public virtual LogStore, + public virtual QueryActiveBuildsStore { using Config = RemoteStoreConfig; @@ -143,6 +147,8 @@ struct RemoteStore : public virtual Store, public virtual GcStore, public virtua void addBuildLog(const StorePath & drvPath, std::string_view log) override; + std::vector queryActiveBuilds() override; + std::optional getVersion() override; void connect() override; diff --git a/src/libstore/include/nix/store/worker-protocol.hh b/src/libstore/include/nix/store/worker-protocol.hh index 79d59144cdf..5105ef20207 100644 --- a/src/libstore/include/nix/store/worker-protocol.hh +++ b/src/libstore/include/nix/store/worker-protocol.hh @@ -138,6 +138,8 @@ struct WorkerProto using Feature = std::string; using FeatureSet = std::set>; + static constexpr std::string_view featureQueryActiveBuilds{"queryActiveBuilds"}; + static const FeatureSet allFeatures; }; @@ -186,6 +188,7 @@ enum struct WorkerProto::Op : uint64_t { AddBuildLog = 45, BuildPathsWithResults = 46, AddPermRoot = 47, + QueryActiveBuilds = 48, }; struct WorkerProto::ClientHandshakeInfo diff --git a/src/libstore/local-store-active-builds.cc b/src/libstore/local-store-active-builds.cc new file mode 100644 index 00000000000..25c6ece5897 --- /dev/null +++ b/src/libstore/local-store-active-builds.cc @@ -0,0 +1,282 @@ +#include "nix/store/local-store.hh" +#include "nix/util/json-utils.hh" +#ifdef __linux__ +# include "nix/util/cgroup.hh" +# include +# include +# include +#endif + +#ifdef __APPLE__ +# include +# include +# include +#endif + +#include +#include + +namespace nix { + +#ifdef __linux__ +static ActiveBuildInfo::ProcessInfo getProcessInfo(pid_t pid) +{ + ActiveBuildInfo::ProcessInfo info; + info.pid = pid; + info.argv = + tokenizeString>(readFile(fmt("/proc/%d/cmdline", pid)), std::string("\000", 1)); + + auto statPath = fmt("/proc/%d/stat", pid); + + AutoCloseFD statFd = open(statPath.c_str(), O_RDONLY | O_CLOEXEC); + if (!statFd) + throw SysError("opening '%s'", statPath); + + // Get the UID from the ownership of the stat file. + struct stat st; + if (fstat(statFd.get(), &st) == -1) + throw SysError("getting ownership of '%s'", statPath); + info.user = UserInfo::fromUid(st.st_uid); + + // Read /proc/[pid]/stat for parent PID and CPU times. + // Format: pid (comm) state ppid ... + // Note that the comm field can contain spaces, so use a regex to parse it. + auto statContent = trim(readFile(statFd.get())); + static std::regex statRegex(R"((\d+) \(([^)]*)\) (.*))"); + std::smatch match; + if (!std::regex_match(statContent, match, statRegex)) + throw Error("failed to parse /proc/%d/stat", pid); + + // Parse the remaining fields after (comm). + auto remainingFields = tokenizeString>(match[3].str()); + + if (remainingFields.size() > 1) + info.parentPid = string2Int(remainingFields[1]).value_or(0); + + static long clkTck = sysconf(_SC_CLK_TCK); + if (remainingFields.size() > 14 && clkTck > 0) { + if (auto utime = string2Int(remainingFields[11])) + info.utime = std::chrono::microseconds((*utime * 1'000'000) / clkTck); + if (auto stime = string2Int(remainingFields[12])) + info.stime = std::chrono::microseconds((*stime * 1'000'000) / clkTck); + if (auto cutime = string2Int(remainingFields[13])) + info.cutime = std::chrono::microseconds((*cutime * 1'000'000) / clkTck); + if (auto cstime = string2Int(remainingFields[14])) + info.cstime = std::chrono::microseconds((*cstime * 1'000'000) / clkTck); + } + + return info; +} + +/** + * Recursively get all descendant PIDs of a given PID using /proc/[pid]/task/[pid]/children. + */ +static std::set getDescendantPids(pid_t pid) +{ + std::set descendants; + + [&](this auto self, pid_t pid) -> void { + try { + descendants.insert(pid); + for (const auto & childPidStr : + tokenizeString>(readFile(fmt("/proc/%d/task/%d/children", pid, pid)))) + if (auto childPid = string2Int(childPidStr)) + self(*childPid); + } catch (...) { + // Process may have exited. + ignoreExceptionExceptInterrupt(); + } + }(pid); + + return descendants; +} +#endif + +#ifdef __APPLE__ +static ActiveBuildInfo::ProcessInfo getProcessInfo(pid_t pid) +{ + ActiveBuildInfo::ProcessInfo info; + info.pid = pid; + + // Get basic process info including ppid and uid. + struct proc_bsdinfo procInfo; + if (proc_pidinfo(pid, PROC_PIDTBSDINFO, 0, &procInfo, sizeof(procInfo)) != sizeof(procInfo)) + throw SysError("getting process info for pid %d", pid); + + info.parentPid = procInfo.pbi_ppid; + info.user = UserInfo::fromUid(procInfo.pbi_uid); + + // Get CPU times. + struct proc_taskinfo taskInfo; + if (proc_pidinfo(pid, PROC_PIDTASKINFO, 0, &taskInfo, sizeof(taskInfo)) == sizeof(taskInfo)) { + + mach_timebase_info_data_t timebase; + mach_timebase_info(&timebase); + auto nanosecondsPerTick = (double) timebase.numer / (double) timebase.denom; + + // Convert nanoseconds to microseconds. + info.utime = + std::chrono::microseconds((uint64_t) ((double) taskInfo.pti_total_user * nanosecondsPerTick / 1000)); + info.stime = + std::chrono::microseconds((uint64_t) ((double) taskInfo.pti_total_system * nanosecondsPerTick / 1000)); + } + + // Get argv using sysctl. + int mib[3] = {CTL_KERN, KERN_PROCARGS2, pid}; + size_t size = 0; + + // First call to get size. + if (sysctl(mib, 3, nullptr, &size, nullptr, 0) == 0 && size > 0) { + std::vector buffer(size); + if (sysctl(mib, 3, buffer.data(), &size, nullptr, 0) == 0) { + // Format: argc (int), followed by executable path, followed by null-terminated args + if (size >= sizeof(int)) { + int argc; + memcpy(&argc, buffer.data(), sizeof(argc)); + + // Skip past argc and executable path (null-terminated). + size_t pos = sizeof(int); + while (pos < size && buffer[pos] != '\0') + pos++; + pos++; // Skip the null terminator + + // Parse the arguments. + while (pos < size && info.argv.size() < (size_t) argc) { + size_t argStart = pos; + while (pos < size && buffer[pos] != '\0') + pos++; + + if (pos > argStart) + info.argv.emplace_back(buffer.data() + argStart, pos - argStart); + + pos++; // Skip the null terminator + } + } + } + } + + return info; +} + +/** + * Recursively get all descendant PIDs using sysctl with KERN_PROC. + */ +static std::set getDescendantPids(pid_t startPid) +{ + // Get all processes. + int mib[4] = {CTL_KERN, KERN_PROC, KERN_PROC_ALL, 0}; + size_t size = 0; + + if (sysctl(mib, 4, nullptr, &size, nullptr, 0) == -1) + return {startPid}; + + std::vector procs(size / sizeof(struct kinfo_proc)); + if (sysctl(mib, 4, procs.data(), &size, nullptr, 0) == -1) + return {startPid}; + + // Get the children of all processes. + std::map> children; + size_t count = size / sizeof(struct kinfo_proc); + for (size_t i = 0; i < count; i++) { + pid_t childPid = procs[i].kp_proc.p_pid; + pid_t parentPid = procs[i].kp_eproc.e_ppid; + children[parentPid].insert(childPid); + } + + // Get all children of `pid`. + std::set descendants; + std::queue todo; + todo.push(startPid); + while (auto pid = pop(todo)) { + if (!descendants.insert(*pid).second) + continue; + for (auto & child : children[*pid]) + todo.push(child); + } + + return descendants; +} +#endif + +std::vector LocalStore::queryActiveBuilds() +{ + std::vector result; + + for (auto & entry : DirectoryIterator{activeBuildsDir}) { + auto path = entry.path(); + + try { + // Open the file. If we can lock it, the build is not active. + auto fd = openLockFile(path, false); + if (!fd || lockFile(fd.get(), ltRead, false)) { + AutoDelete(path, false); + continue; + } + + ActiveBuildInfo info(nlohmann::json::parse(readFile(fd.get())).get()); + +#if defined(__linux__) || defined(__APPLE__) + /* Read process information. */ + try { +# ifdef __linux__ + if (info.cgroup) { + for (auto pid : getPidsInCgroup(*info.cgroup)) + info.processes.push_back(getProcessInfo(pid)); + + /* Read CPU statistics from the cgroup. */ + auto stats = getCgroupStats(*info.cgroup); + info.utime = stats.cpuUser; + info.stime = stats.cpuSystem; + } else +# endif + { + for (auto pid : getDescendantPids(info.mainPid)) + info.processes.push_back(getProcessInfo(pid)); + } + } catch (...) { + ignoreExceptionExceptInterrupt(); + } +#endif + + result.push_back(std::move(info)); + } catch (...) { + ignoreExceptionExceptInterrupt(); + } + } + + return result; +} + +LocalStore::BuildHandle LocalStore::buildStarted(const ActiveBuild & build) +{ + // Write info about the active build to the active-builds directory where it can be read by `queryBuilds()`. + static std::atomic nextId{1}; + + auto id = nextId++; + + auto infoFileName = fmt("%d-%d", getpid(), id); + auto infoFilePath = activeBuildsDir / infoFileName; + + auto infoFd = openLockFile(infoFilePath, true); + + // Lock the file to denote that the build is active. + lockFile(infoFd.get(), ltWrite, true); + + writeFile(infoFilePath, nlohmann::json(build).dump(), 0600, FsSync::Yes); + + activeBuilds.lock()->emplace( + id, + ActiveBuildFile{ + .fd = std::move(infoFd), + .del = AutoDelete(infoFilePath, false), + }); + + return BuildHandle(*this, id); +} + +void LocalStore::buildFinished(const BuildHandle & handle) +{ + activeBuilds.lock()->erase(handle.id); +} + +} // namespace nix diff --git a/src/libstore/local-store.cc b/src/libstore/local-store.cc index 5c8c5f1575a..c4b9fde310c 100644 --- a/src/libstore/local-store.cc +++ b/src/libstore/local-store.cc @@ -125,6 +125,7 @@ LocalStore::LocalStore(ref config) , schemaPath(dbDir + "/schema") , tempRootsDir(config->stateDir + "/temproots") , fnTempRoots(fmt("%s/%d", tempRootsDir, getpid())) + , activeBuildsDir(config->stateDir + "/active-builds") { auto state(_state->lock()); state->stmts = std::make_unique(); @@ -146,6 +147,7 @@ LocalStore::LocalStore(ref config) createDirs(gcRootsDir); replaceSymlink(profilesDir, gcRootsDir + "/profiles"); } + createDirs(activeBuildsDir); for (auto & perUserDir : {profilesDir + "/per-user", gcRootsDir + "/per-user"}) { createDirs(perUserDir); diff --git a/src/libstore/meson.build b/src/libstore/meson.build index 5ca54930df6..c23bc28a177 100644 --- a/src/libstore/meson.build +++ b/src/libstore/meson.build @@ -300,6 +300,7 @@ subdir('nix-meson-build-support/common') subdir('nix-meson-build-support/asan-options') sources = files( + 'active-builds.cc', 'async-path-writer.cc', 'aws-creds.cc', 'binary-cache-store.cc', @@ -338,6 +339,7 @@ sources = files( 'local-binary-cache-store.cc', 'local-fs-store.cc', 'local-overlay-store.cc', + 'local-store-active-builds.cc', 'local-store.cc', 'log-store.cc', 'machines.cc', diff --git a/src/libstore/remote-store.cc b/src/libstore/remote-store.cc index e78d8e2ff67..045d518b979 100644 --- a/src/libstore/remote-store.cc +++ b/src/libstore/remote-store.cc @@ -763,6 +763,16 @@ void RemoteStore::addBuildLog(const StorePath & drvPath, std::string_view log) readInt(conn->from); } +std::vector RemoteStore::queryActiveBuilds() +{ + auto conn(getConnection()); + if (!conn->features.count(WorkerProto::featureQueryActiveBuilds)) + throw Error("remote store does not support querying active builds"); + conn->to << WorkerProto::Op::QueryActiveBuilds; + conn.processStderr(); + return nlohmann::json::parse(readString(conn->from)).get>(); +} + std::optional RemoteStore::getVersion() { auto conn(getConnection()); diff --git a/src/libstore/unix/build/derivation-builder.cc b/src/libstore/unix/build/derivation-builder.cc index 1477aac0c05..a78677f213f 100644 --- a/src/libstore/unix/build/derivation-builder.cc +++ b/src/libstore/unix/build/derivation-builder.cc @@ -1,6 +1,7 @@ #include "nix/store/build/derivation-builder.hh" #include "nix/util/file-system.hh" #include "nix/store/local-store.hh" +#include "nix/store/active-builds.hh" #include "nix/util/processes.hh" #include "nix/store/builtins.hh" #include "nix/store/path-references.hh" @@ -77,6 +78,11 @@ class DerivationBuilderImpl : public DerivationBuilder, public DerivationBuilder */ Pid pid; + /** + * Handles to track active builds for `nix ps`. + */ + std::optional activeBuildHandle; + LocalStore & store; std::unique_ptr miscMethods; @@ -235,6 +241,11 @@ class DerivationBuilderImpl : public DerivationBuilder, public DerivationBuilder */ virtual void checkSystem(); + /** + * Construct the `ActiveBuild` object for `ActiveBuildsTracker`. + */ + virtual ActiveBuild getActiveBuild(); + /** * Return the paths that should be made available in the sandbox. * This includes: @@ -471,6 +482,8 @@ bool DerivationBuilderImpl::killChild() killSandbox(true); pid.wait(); + + activeBuildHandle.reset(); } return ret; } @@ -504,6 +517,8 @@ SingleDrvOutputs DerivationBuilderImpl::unprepareBuild() root. */ killSandbox(true); + activeBuildHandle.reset(); + /* Terminate the recursive Nix daemon. */ stopDaemon(); @@ -847,11 +862,28 @@ std::optional DerivationBuilderImpl::startBuild() pid.setSeparatePG(true); + /* Make the build visible to `nix ps`. */ + if (auto tracker = dynamic_cast(&store)) + activeBuildHandle.emplace(tracker->buildStarted(getActiveBuild())); + processSandboxSetupMessages(); return builderOut.get(); } +ActiveBuild DerivationBuilderImpl::getActiveBuild() +{ + return { + .nixPid = getpid(), + .clientPid = std::nullopt, // FIXME + .clientUid = std::nullopt, // FIXME + .mainPid = pid, + .mainUser = UserInfo::fromUid(buildUser ? buildUser->getUID() : getuid()), + .startTime = buildResult.startTime, + .derivation = drvPath, + }; +} + PathsInChroot DerivationBuilderImpl::getPathsInSandbox() { /* Allow a user-configurable set of directories from the diff --git a/src/libstore/unix/build/linux-derivation-builder.cc b/src/libstore/unix/build/linux-derivation-builder.cc index b89c03890ab..7c6edca6567 100644 --- a/src/libstore/unix/build/linux-derivation-builder.cc +++ b/src/libstore/unix/build/linux-derivation-builder.cc @@ -730,6 +730,13 @@ struct ChrootLinuxDerivationBuilder : ChrootDerivationBuilder, LinuxDerivationBu if (status != 0) throw Error("could not add path '%s' to sandbox", store.printStorePath(path)); } + + ActiveBuild getActiveBuild() override + { + auto build = DerivationBuilderImpl::getActiveBuild(); + build.cgroup = cgroup; + return build; + } }; } // namespace nix diff --git a/src/libstore/worker-protocol-connection.cc b/src/libstore/worker-protocol-connection.cc index 8a37662904d..24d1ea82395 100644 --- a/src/libstore/worker-protocol-connection.cc +++ b/src/libstore/worker-protocol-connection.cc @@ -5,7 +5,7 @@ namespace nix { -const WorkerProto::FeatureSet WorkerProto::allFeatures{}; +const WorkerProto::FeatureSet WorkerProto::allFeatures{{std::string(WorkerProto::featureQueryActiveBuilds)}}; WorkerProto::BasicClientConnection::~BasicClientConnection() { diff --git a/src/libutil/include/nix/util/file-system.hh b/src/libutil/include/nix/util/file-system.hh index 98b9924721a..67d4ba0250a 100644 --- a/src/libutil/include/nix/util/file-system.hh +++ b/src/libutil/include/nix/util/file-system.hh @@ -286,6 +286,15 @@ class AutoDelete bool recursive; public: AutoDelete(); + + AutoDelete(AutoDelete && x) + { + _path = std::move(x._path); + del = x.del; + recursive = x.recursive; + x.del = false; + } + AutoDelete(const std::filesystem::path & p, bool recursive = true); ~AutoDelete(); diff --git a/src/libutil/include/nix/util/meson.build b/src/libutil/include/nix/util/meson.build index eebbf0f6afe..8ec06b2c45c 100644 --- a/src/libutil/include/nix/util/meson.build +++ b/src/libutil/include/nix/util/meson.build @@ -73,6 +73,7 @@ headers = files( 'strings.hh', 'suggestions.hh', 'sync.hh', + 'table.hh', 'tarfile.hh', 'terminal.hh', 'thread-pool.hh', diff --git a/src/libutil/include/nix/util/table.hh b/src/libutil/include/nix/util/table.hh new file mode 100644 index 00000000000..0af33b66cc3 --- /dev/null +++ b/src/libutil/include/nix/util/table.hh @@ -0,0 +1,27 @@ +#pragma once + +#include "nix/util/types.hh" + +#include + +namespace nix { + +struct TableCell +{ + std::string content; + + enum Alignment { Left, Right } alignment = Left; + + TableCell(std::string content, Alignment alignment = Left) + : content(std::move(content)) + , alignment(alignment) + { + } +}; + +using TableRow = std::vector; +using Table = std::vector; + +void printTable(std::ostream & out, Table & table, unsigned int width = std::numeric_limits::max()); + +} // namespace nix diff --git a/src/libutil/include/nix/util/terminal.hh b/src/libutil/include/nix/util/terminal.hh index fa71e074e6c..a09b71c5277 100644 --- a/src/libutil/include/nix/util/terminal.hh +++ b/src/libutil/include/nix/util/terminal.hh @@ -36,6 +36,11 @@ void updateWindowSize(); */ std::pair getWindowSize(); +/** + * @return The number of columns of the terminal, or std::numeric_limits::max() if unknown. + */ +unsigned int getWindowWidth(); + /** * Get the slave name of a pseudoterminal in a thread-safe manner. * diff --git a/src/libutil/linux/cgroup.cc b/src/libutil/linux/cgroup.cc index 9e78ac6d2ae..802b56336d1 100644 --- a/src/libutil/linux/cgroup.cc +++ b/src/libutil/linux/cgroup.cc @@ -49,6 +49,33 @@ StringMap getCgroups(const Path & cgroupFile) return cgroups; } +CgroupStats getCgroupStats(const std::filesystem::path & cgroup) +{ + CgroupStats stats; + + auto cpustatPath = cgroup / "cpu.stat"; + + if (pathExists(cpustatPath)) { + for (auto & line : tokenizeString>(readFile(cpustatPath), "\n")) { + std::string_view userPrefix = "user_usec "; + if (hasPrefix(line, userPrefix)) { + auto n = string2Int(line.substr(userPrefix.size())); + if (n) + stats.cpuUser = std::chrono::microseconds(*n); + } + + std::string_view systemPrefix = "system_usec "; + if (hasPrefix(line, systemPrefix)) { + auto n = string2Int(line.substr(systemPrefix.size())); + if (n) + stats.cpuSystem = std::chrono::microseconds(*n); + } + } + } + + return stats; +} + static CgroupStats destroyCgroup(const std::filesystem::path & cgroup, bool returnStats) { if (!pathExists(cgroup)) @@ -114,28 +141,8 @@ static CgroupStats destroyCgroup(const std::filesystem::path & cgroup, bool retu } CgroupStats stats; - - if (returnStats) { - auto cpustatPath = cgroup / "cpu.stat"; - - if (pathExists(cpustatPath)) { - for (auto & line : tokenizeString>(readFile(cpustatPath), "\n")) { - std::string_view userPrefix = "user_usec "; - if (hasPrefix(line, userPrefix)) { - auto n = string2Int(line.substr(userPrefix.size())); - if (n) - stats.cpuUser = std::chrono::microseconds(*n); - } - - std::string_view systemPrefix = "system_usec "; - if (hasPrefix(line, systemPrefix)) { - auto n = string2Int(line.substr(systemPrefix.size())); - if (n) - stats.cpuSystem = std::chrono::microseconds(*n); - } - } - } - } + if (returnStats) + stats = getCgroupStats(cgroup); if (rmdir(cgroup.c_str()) == -1) throw SysError("deleting cgroup %s", cgroup); @@ -167,4 +174,23 @@ std::string getRootCgroup() return rootCgroup; } +std::set getPidsInCgroup(const std::filesystem::path & cgroup) +{ + if (!pathExists(cgroup)) + return {}; + + auto procsFile = cgroup / "cgroup.procs"; + + std::set result; + + for (auto & pidStr : tokenizeString>(readFile(procsFile))) { + if (auto o = string2Int(pidStr)) + result.insert(*o); + else + throw Error("invalid PID '%s'", pidStr); + } + + return result; +} + } // namespace nix diff --git a/src/libutil/linux/include/nix/util/cgroup.hh b/src/libutil/linux/include/nix/util/cgroup.hh index 59de13d46b9..ad777347670 100644 --- a/src/libutil/linux/include/nix/util/cgroup.hh +++ b/src/libutil/linux/include/nix/util/cgroup.hh @@ -3,6 +3,7 @@ #include #include +#include #include "nix/util/types.hh" @@ -17,6 +18,11 @@ struct CgroupStats std::optional cpuUser, cpuSystem; }; +/** + * Read statistics from the given cgroup. + */ +CgroupStats getCgroupStats(const std::filesystem::path & cgroup); + /** * Destroy the cgroup denoted by 'path'. The postcondition is that * 'path' does not exist, and thus any processes in the cgroup have @@ -34,4 +40,9 @@ std::string getCurrentCgroup(); */ std::string getRootCgroup(); +/** + * Get the PIDs of all processes in the given cgroup. + */ +std::set getPidsInCgroup(const std::filesystem::path & cgroup); + } // namespace nix diff --git a/src/libutil/meson.build b/src/libutil/meson.build index 6c76659dd24..0eab48bcbf2 100644 --- a/src/libutil/meson.build +++ b/src/libutil/meson.build @@ -159,6 +159,7 @@ sources = [ config_priv_h ] + files( 'strings.cc', 'subdir-source-accessor.cc', 'suggestions.cc', + 'table.cc', 'tarfile.cc', 'tee-logger.cc', 'terminal.cc', diff --git a/src/libutil/table.cc b/src/libutil/table.cc new file mode 100644 index 00000000000..215171dc02f --- /dev/null +++ b/src/libutil/table.cc @@ -0,0 +1,51 @@ +#include "nix/util/table.hh" +#include "nix/util/terminal.hh" + +#include +#include +#include +#include + +namespace nix { + +void printTable(std::ostream & out, Table & table, unsigned int width) +{ + auto nrColumns = table.size() > 0 ? table.front().size() : 0; + + std::vector widths; + widths.resize(nrColumns); + + for (auto & i : table) { + assert(i.size() == nrColumns); + size_t column = 0; + for (auto j = i.begin(); j != i.end(); ++j, ++column) + // TODO: take ANSI escapes into account when calculating width. + widths[column] = std::max(widths[column], j->content.size()); + } + + for (auto & i : table) { + size_t column = 0; + std::string line; + for (auto j = i.begin(); j != i.end(); ++j, ++column) { + std::string s = j->content; + replace(s.begin(), s.end(), '\n', ' '); + + auto padding = std::string(widths[column] - s.size(), ' '); + if (j->alignment == TableCell::Right) { + line += padding; + line += s; + } else { + line += s; + if (column + 1 < nrColumns) + line += padding; + } + + if (column + 1 < nrColumns) + line += " "; + } + out << filterANSIEscapes(line, false, width); + out << std::endl; + } +} + +} // namespace nix diff --git a/src/libutil/terminal.cc b/src/libutil/terminal.cc index fe22146abb0..a00892ac047 100644 --- a/src/libutil/terminal.cc +++ b/src/libutil/terminal.cc @@ -179,6 +179,14 @@ std::pair getWindowSize() return *windowSize.lock(); } +unsigned int getWindowWidth() +{ + unsigned int width = getWindowSize().second; + if (width <= 0) + width = std::numeric_limits::max(); + return width; +} + #ifndef _WIN32 std::string getPtsName(int fd) { diff --git a/src/nix/meson.build b/src/nix/meson.build index 4b4b85eb62a..3fdc1fcb2c0 100644 --- a/src/nix/meson.build +++ b/src/nix/meson.build @@ -96,6 +96,7 @@ nix_sources = [ config_priv_h ] + files( 'path-info.cc', 'prefetch.cc', 'profile.cc', + 'ps.cc', 'realisation.cc', 'registry.cc', 'repl.cc', diff --git a/src/nix/nix-env/nix-env.cc b/src/nix/nix-env/nix-env.cc index de437cc1c89..ee2458b10aa 100644 --- a/src/nix/nix-env/nix-env.cc +++ b/src/nix/nix-env/nix-env.cc @@ -16,6 +16,7 @@ #include "nix/util/xml-writer.hh" #include "nix/cmd/legacy.hh" #include "nix/expr/eval-settings.hh" // for defexpr +#include "nix/util/table.hh" #include "nix/util/terminal.hh" #include "man-pages.hh" @@ -824,38 +825,6 @@ static bool cmpElemByName(const PackageInfo & a, const PackageInfo & b) return lexicographical_compare(a_name.begin(), a_name.end(), b_name.begin(), b_name.end(), cmpChars); } -typedef std::list Table; - -void printTable(Table & table) -{ - auto nrColumns = table.size() > 0 ? table.front().size() : 0; - - std::vector widths; - widths.resize(nrColumns); - - for (auto & i : table) { - assert(i.size() == nrColumns); - Strings::iterator j; - size_t column; - for (j = i.begin(), column = 0; j != i.end(); ++j, ++column) - if (j->size() > widths[column]) - widths[column] = j->size(); - } - - for (auto & i : table) { - Strings::iterator j; - size_t column; - for (j = i.begin(), column = 0; j != i.end(); ++j, ++column) { - std::string s = *j; - replace(s.begin(), s.end(), '\n', ' '); - cout << s; - if (column < nrColumns - 1) - cout << std::string(widths[column] - s.size() + 2, ' '); - } - cout << std::endl; - } -} - /* This function compares the version of an element against the versions in the given set of elements. `cvLess' means that only lower versions are in the set, `cvEqual' means that at most an @@ -1095,7 +1064,7 @@ static void opQuery(Globals & globals, Strings opFlags, Strings opArgs) continue; /* For table output. */ - Strings columns; + TableRow columns; /* For XML output. */ XMLAttrs attrs; @@ -1283,7 +1252,7 @@ static void opQuery(Globals & globals, Strings opFlags, Strings opArgs) } if (!xmlOutput) - printTable(table); + printTable(std::cout, table); } static void opSwitchProfile(Globals & globals, Strings opFlags, Strings opArgs) diff --git a/src/nix/ps.cc b/src/nix/ps.cc new file mode 100644 index 00000000000..9ae9d97bf98 --- /dev/null +++ b/src/nix/ps.cc @@ -0,0 +1,146 @@ +#include "nix/cmd/command.hh" +#include "nix/main/common-args.hh" +#include "nix/main/shared.hh" +#include "nix/store/store-api.hh" +#include "nix/store/store-cast.hh" +#include "nix/store/active-builds.hh" +#include "nix/util/table.hh" +#include "nix/util/terminal.hh" + +#include + +using namespace nix; + +struct CmdPs : MixJSON, StoreCommand +{ + std::string description() override + { + return "list active builds"; + } + + Category category() override + { + return catUtility; + } + + std::string doc() override + { + return +#include "ps.md" + ; + } + + void run(ref store) override + { + auto & tracker = require(*store); + + auto builds = tracker.queryActiveBuilds(); + + if (json) { + printJSON(nlohmann::json(builds)); + return; + } + + if (builds.empty()) { + notice("No active builds."); + return; + } + + /* Helper to format user info: show name if available, else UID */ + auto formatUser = [](const UserInfo & user) -> std::string { + return user.name ? *user.name : std::to_string(user.uid); + }; + + Table table; + + /* Add column headers. */ + table.push_back({{"USER"}, {"PID"}, {"CPU", TableCell::Alignment::Right}, {"DERIVATION/COMMAND"}}); + + for (const auto & build : builds) { + /* Calculate CPU time - use cgroup stats if available, otherwise sum process times. */ + std::chrono::microseconds cpuTime = build.utime && build.stime ? *build.utime + *build.stime : [&]() { + std::chrono::microseconds total{0}; + for (const auto & process : build.processes) + total += process.utime.value_or(std::chrono::microseconds(0)) + + process.stime.value_or(std::chrono::microseconds(0)) + + process.cutime.value_or(std::chrono::microseconds(0)) + + process.cstime.value_or(std::chrono::microseconds(0)); + return total; + }(); + + /* Add build summary row. */ + table.push_back( + {formatUser(build.mainUser), + std::to_string(build.mainPid), + {fmt("%.1fs", + std::chrono::duration_cast>(cpuTime) + .count()), + TableCell::Alignment::Right}, + fmt(ANSI_BOLD "%s" ANSI_NORMAL " (wall=%ds)", + store->printStorePath(build.derivation), + time(nullptr) - build.startTime)}); + + if (build.processes.empty()) { + table.push_back( + {formatUser(build.mainUser), + std::to_string(build.mainPid), + {"", TableCell::Alignment::Right}, + fmt("%s" ANSI_ITALIC "(no process info)" ANSI_NORMAL, treeLast)}); + } else { + /* Recover the tree structure of the processes. */ + std::set pids; + for (auto & process : build.processes) + pids.insert(process.pid); + + using Processes = std::set; + std::map children; + Processes rootProcesses; + for (auto & process : build.processes) { + if (pids.contains(process.parentPid)) + children[process.parentPid].insert(&process); + else + rootProcesses.insert(&process); + } + + /* Render the process tree. */ + [&](this auto const & visit, const Processes & processes, std::string_view prefix) -> void { + for (const auto & [n, process] : enumerate(processes)) { + bool last = n + 1 == processes.size(); + + // Format CPU time if available + std::string cpuInfo; + if (process->utime || process->stime || process->cutime || process->cstime) { + auto totalCpu = process->utime.value_or(std::chrono::microseconds(0)) + + process->stime.value_or(std::chrono::microseconds(0)) + + process->cutime.value_or(std::chrono::microseconds(0)) + + process->cstime.value_or(std::chrono::microseconds(0)); + auto totalSecs = + std::chrono::duration_cast>( + totalCpu) + .count(); + cpuInfo = fmt("%.1fs", totalSecs); + } + + // Format argv with tree structure + auto argv = concatStringsSep( + " ", tokenizeString>(concatStringsSep(" ", process->argv))); + + table.push_back( + {formatUser(process->user), + std::to_string(process->pid), + {cpuInfo, TableCell::Alignment::Right}, + fmt("%s%s%s", prefix, last ? treeLast : treeConn, argv)}); + + visit(children[process->pid], last ? prefix + treeNull : prefix + treeLine); + } + }(rootProcesses, ""); + } + } + + auto width = isTTY() && isatty(STDOUT_FILENO) ? getWindowWidth() : std::numeric_limits::max(); + + printTable(std::cout, table, width); + } +}; + +static auto rCmdPs = registerCommand2({"ps"}); diff --git a/src/nix/ps.md b/src/nix/ps.md new file mode 100644 index 00000000000..07decb61622 --- /dev/null +++ b/src/nix/ps.md @@ -0,0 +1,27 @@ +R"( + +# Examples + +* Show all active builds: + + ```console + # nix ps + USER PID CPU DERIVATION/COMMAND + nixbld11 3534394 110.2s /nix/store/lzvdxlbr6xjd9w8py4nd2y2nnqb9gz7p-nix-util-tests-3.13.2.drv (wall=8s) + nixbld11 3534394 0.8s └───bash -e /nix/store/vj1c3wf9c11a0qs6p3ymfvrnsdgsdcbq-source-stdenv.sh /nix/store/shkw4qm9qcw5sc5n1k5jznc83ny02 + nixbld11 3534751 36.3s └───ninja -j24 + nixbld11 3535637 0.0s ├───/nix/store/8adzgnxs3s0pbj22qhk9zjxi1fqmz3xv-gcc-14.3.0/bin/g++ -fPIC -fstack-clash-protection -O2 -U_ + nixbld11 3535639 0.1s │ └───/nix/store/8adzgnxs3s0pbj22qhk9zjxi1fqmz3xv-gcc-14.3.0/libexec/gcc/x86_64-unknown-linux-gnu/14.3. + nixbld11 3535658 0.0s └───/nix/store/8adzgnxs3s0pbj22qhk9zjxi1fqmz3xv-gcc-14.3.0/bin/g++ -fPIC -fstack-clash-protection -O2 -U_ + nixbld1 3534377 1.8s /nix/store/nh2dx9cqcy9lw4d4rvd0dbsflwdsbzdy-patchelf-0.18.0.drv (wall=5s) + nixbld1 3534377 1.8s └───bash -e /nix/store/v6x3cs394jgqfbi0a42pam708flxaphh-default-builder.sh + nixbld1 3535074 0.0s └───/nix/store/0irlcqx2n3qm6b1pc9rsd2i8qpvcccaj-bash-5.2p37/bin/bash ./configure --disable-dependency-trackin + ``` + +# Description + +This command lists all currently running Nix builds. +For each build, it shows the derivation path and the main process ID. +On Linux and macOS, it also shows the child processes of each build. + +)"