Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
4ddb8b3
AutoDelete: Add move constructor
edolstra Nov 27, 2025
8b57a52
Add basic `nix ps` command
edolstra Nov 27, 2025
0f7b5b1
nix ps: Use cgroups to get all processes of a build
edolstra Nov 27, 2025
b5e4544
nix ps: Render the processes in a cgroup as a tree
edolstra Nov 27, 2025
367b81d
Move terminal width calculation to libutil
edolstra Nov 27, 2025
d0daa4c
nix ps: Adapt to terminal width
edolstra Nov 27, 2025
5b8fb29
nix ps: Support RemoteStore
edolstra Nov 27, 2025
274b793
coderabbit review
edolstra Nov 27, 2025
77e6c09
Move code
edolstra Nov 27, 2025
33eab7f
Add worker protocol feature for queryActiveBuilds
edolstra Nov 28, 2025
c7e4be8
nix ps: Show how long a build has been running
edolstra Nov 28, 2025
b3aef46
Add getCgroupStats() function
edolstra Nov 30, 2025
56824b8
nix ps: Show cgroup CPU stats
edolstra Nov 30, 2025
ada2fd4
Return per-process CPU time
edolstra Dec 1, 2025
de7101c
TODO
edolstra Dec 1, 2025
ec265c5
nix ps: Improve output formatting
edolstra Dec 1, 2025
45b0ab5
Get per-process uid
edolstra Dec 1, 2025
8cb1de9
Return user names if they exist
edolstra Dec 1, 2025
0e604bc
When not using cgroups, use /proc to enumerate the children of a pid
edolstra Dec 1, 2025
c10a68f
Move table stuff into libutil
edolstra Dec 1, 2025
2416099
Table: Use std::vectors
edolstra Dec 1, 2025
d4e5956
printTable(): Make destination stream explicit
edolstra Dec 1, 2025
09fa833
Code review
edolstra Dec 2, 2025
0910a53
Improve /proc/pid/stat parsing
edolstra Dec 2, 2025
c744ac3
nix ps: Use printTable()
edolstra Dec 2, 2025
ef95463
Rename cpuUser, cpuSystem to utime, stime
edolstra Dec 3, 2025
d60890d
Return cutime and cstime
edolstra Dec 3, 2025
d22ad22
nix ps: Add --json flag
edolstra Dec 3, 2025
b6336cc
nix ps: Update example
edolstra Dec 3, 2025
906cd9d
nix ps: Improve formatting
edolstra Dec 3, 2025
c02a018
Serialize durations as floating-point seconds
edolstra Dec 3, 2025
3771d17
nix ps: macOS support
edolstra Dec 3, 2025
33a856f
Review comments
edolstra Dec 3, 2025
72abd2a
nix ps: Check whether stdout is a tty
edolstra Dec 4, 2025
383aec2
Remove questionable code
edolstra Dec 5, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/libfetchers/git-utils.cc
Original file line number Diff line number Diff line change
Expand Up @@ -359,7 +359,7 @@ struct GitRepoImpl : GitRepo, std::enable_shared_from_this<GitRepoImpl>

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);
Expand Down
6 changes: 1 addition & 5 deletions src/libmain/progress-bar.cc
Original file line number Diff line number Diff line change
Expand Up @@ -470,11 +470,7 @@ class ProgressBar : public Logger
}
}

auto width = getWindowSize().second;
if (width <= 0)
width = std::numeric_limits<decltype(width)>::max();

redraw("\r" + filterANSIEscapes(line, false, width) + ANSI_NORMAL + "\e[K");
redraw("\r" + filterANSIEscapes(line, false, getWindowWidth()) + ANSI_NORMAL + "\e[K");

return nextWakeup;
}
Expand Down
145 changes: 145 additions & 0 deletions src/libstore/active-builds.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
#include "nix/store/active-builds.hh"
#include "nix/util/json-utils.hh"

#include <nlohmann/json.hpp>

#ifndef _WIN32
# include <pwd.h>
#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<char> 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<UserInfo>::from_json(const json & j)
{
return UserInfo{
.uid = j.at("uid").get<uid_t>(),
.name = j.contains("name") && !j.at("name").is_null()
? std::optional<std::string>(j.at("name").get<std::string>())
: std::nullopt,
};
}

void adl_serializer<UserInfo>::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<std::chrono::microseconds> parseDuration(const json & j, const char * key)
{
if (j.contains(key) && !j.at(key).is_null())
return std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::duration<float, std::chrono::seconds::period>(j.at(key).get<double>()));
else
return std::nullopt;
}

static nlohmann::json printDuration(const std::optional<std::chrono::microseconds> & duration)
{
return duration
? nlohmann::json(
std::chrono::duration_cast<std::chrono::duration<float, std::chrono::seconds::period>>(*duration)
.count())
: nullptr;
}

ActiveBuildInfo::ProcessInfo adl_serializer<ActiveBuildInfo::ProcessInfo>::from_json(const json & j)
{
return ActiveBuildInfo::ProcessInfo{
.pid = j.at("pid").get<pid_t>(),
.parentPid = j.at("parentPid").get<pid_t>(),
.user = j.at("user").get<UserInfo>(),
.argv = j.at("argv").get<std::vector<std::string>>(),
.utime = parseDuration(j, "utime"),
.stime = parseDuration(j, "stime"),
.cutime = parseDuration(j, "cutime"),
.cstime = parseDuration(j, "cstime"),
};
}

void adl_serializer<ActiveBuildInfo::ProcessInfo>::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<ActiveBuild>::from_json(const json & j)
{
return ActiveBuild{
.nixPid = j.at("nixPid").get<pid_t>(),
.clientPid = j.at("clientPid").get<std::optional<pid_t>>(),
.clientUid = j.at("clientUid").get<std::optional<uid_t>>(),
.mainPid = j.at("mainPid").get<pid_t>(),
.mainUser = j.at("mainUser").get<UserInfo>(),
.cgroup = j.at("cgroup").get<std::optional<Path>>(),
.startTime = (time_t) j.at("startTime").get<double>(),
.derivation = StorePath{getString(j.at("derivation"))},
};
}

void adl_serializer<ActiveBuild>::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<ActiveBuildInfo>::from_json(const json & j)
{
ActiveBuildInfo info(adl_serializer<ActiveBuild>::from_json(j));
info.processes = j.at("processes").get<std::vector<ActiveBuildInfo::ProcessInfo>>();
info.utime = parseDuration(j, "utime");
info.stime = parseDuration(j, "stime");
return info;
}

void adl_serializer<ActiveBuildInfo>::to_json(json & j, const ActiveBuildInfo & build)
{
adl_serializer<ActiveBuild>::to_json(j, build);
j["processes"] = build.processes;
j["utime"] = printDuration(build.utime);
j["stime"] = printDuration(build.stime);
}

} // namespace nlohmann
10 changes: 10 additions & 0 deletions src/libstore/daemon.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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<QueryActiveBuildsStore>(*store);
auto activeBuilds = activeBuildsStore.queryActiveBuilds();
logger->stopWork();
conn.to << nlohmann::json(activeBuilds).dump();
break;
}

default:
throw Error("invalid operation %1%", op);
}
Expand Down
108 changes: 108 additions & 0 deletions src/libstore/include/nix/store/active-builds.hh
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
#pragma once

#include "nix/util/util.hh"
#include "nix/util/json-impls.hh"
#include "nix/store/path.hh"

#include <chrono>
#include <sys/types.h>

namespace nix {

/**
* A uid and optional corresponding user name.
*/
struct UserInfo
{
uid_t uid = -1;
std::optional<std::string> 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<pid_t> clientPid;
std::optional<uid_t> clientUid;

pid_t mainPid;
UserInfo mainUser;
std::optional<Path> cgroup;

time_t startTime;

StorePath derivation;
};

struct ActiveBuildInfo : ActiveBuild
{
struct ProcessInfo
{
pid_t pid = 0;
pid_t parentPid = 0;
UserInfo user;
std::vector<std::string> argv;
std::optional<std::chrono::microseconds> utime, stime, cutime, cstime;
};

// User/system CPU time for the entire cgroup, if available.
std::optional<std::chrono::microseconds> utime, stime;

std::vector<ProcessInfo> 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<ActiveBuildInfo> queryActiveBuilds() = 0;
};

} // namespace nix

JSON_IMPL(UserInfo)
JSON_IMPL(ActiveBuild)
JSON_IMPL(ActiveBuildInfo)
JSON_IMPL(ActiveBuildInfo::ProcessInfo)
24 changes: 23 additions & 1 deletion src/libstore/include/nix/store/local-store.hh
Original file line number Diff line number Diff line change
Expand Up @@ -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 <chrono>
Expand Down Expand Up @@ -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:

Expand Down Expand Up @@ -457,6 +461,24 @@ private:

friend struct PathSubstitutionGoal;
friend struct DerivationGoal;

private:

std::filesystem::path activeBuildsDir;

struct ActiveBuildFile
{
AutoCloseFD fd;
AutoDelete del;
};

Sync<std::unordered_map<uint64_t, ActiveBuildFile>> activeBuilds;

std::vector<ActiveBuildInfo> queryActiveBuilds() override;

BuildHandle buildStarted(const ActiveBuild & build) override;

void buildFinished(const BuildHandle & handle) override;
};

} // namespace nix
1 change: 1 addition & 0 deletions src/libstore/include/nix/store/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
8 changes: 7 additions & 1 deletion src/libstore/include/nix/store/remote-store.hh
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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<ActiveBuildInfo> queryActiveBuilds() override;

std::optional<std::string> getVersion() override;

void connect() override;
Expand Down
3 changes: 3 additions & 0 deletions src/libstore/include/nix/store/worker-protocol.hh
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,8 @@ struct WorkerProto
using Feature = std::string;
using FeatureSet = std::set<Feature, std::less<>>;

static constexpr std::string_view featureQueryActiveBuilds{"queryActiveBuilds"};

static const FeatureSet allFeatures;
};

Expand Down Expand Up @@ -186,6 +188,7 @@ enum struct WorkerProto::Op : uint64_t {
AddBuildLog = 45,
BuildPathsWithResults = 46,
AddPermRoot = 47,
QueryActiveBuilds = 48,
};

struct WorkerProto::ClientHandshakeInfo
Expand Down
Loading