1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
#include <sstream>

#include "lix/libcmd/command.hh"
#include "lix/libutil/logging.hh"
#include "lix/libstore/serve-protocol.hh"
#include "lix/libmain/shared.hh"
#include "lix/libstore/store-api.hh"
#include "lix/libstore/local-fs-store.hh"
#include "lix/libstore/worker-protocol.hh"
#include "lix/libutil/exit.hh"
#include "lix/libstore/profiles.hh"
#include "doctor.hh"

namespace nix {

namespace {

std::string formatProtocol(unsigned int proto)
{
    if (proto) {
        auto major = GET_PROTOCOL_MAJOR(proto) >> 8;
        auto minor = GET_PROTOCOL_MINOR(proto);
        return fmt("%1%.%2%", major, minor);
    }
    return "unknown";
}

bool checkPass(const std::string & msg) {
    notice(ANSI_GREEN "[PASS] " ANSI_NORMAL "%1%", Uncolored(msg));
    return true;
}

bool checkFail(const std::string & msg) {
    notice(ANSI_RED "[FAIL] " ANSI_NORMAL "%1%", Uncolored(msg));
    return false;
}

void checkInfo(const std::string & msg) {
    notice(ANSI_BLUE "[INFO] " ANSI_NORMAL "%1%", Uncolored(msg));
}

}

struct CmdDoctor : StoreCommand
{
    bool success = true;

    /**
     * This command is stable before the others
     */
    std::optional<ExperimentalFeature> experimentalFeature() override
    {
        return std::nullopt;
    }

    std::string description() override
    {
        return "check your system for potential problems and print a PASS or FAIL for each check";
    }

    Category category() override { return catNixInstallation; }

    void run(ref<Store> store) override
    {
        printInfo("Running checks against store uri %1%", store->getUri());

        if (store.try_cast_shared<LocalFSStore>()) {
            success &= checkNixInPath();
            success &= checkProfileRoots(store);
        }
        success &= checkStoreProtocol(aio().blockOn(store->getProtocol()));
        checkTrustedUser(store);
        {
            Path profile = getEnv("NIX_PROFILE").value_or(getDefaultProfile());
            checkValidCurrentProfileGeneration(profile);
        }

        if (!success)
            throw Exit(2);
    }

    bool checkNixInPath()
    {
        PathSet dirs;

        for (auto & dir : tokenizeString<Strings>(getEnv("PATH").value_or(""), ":"))
            // Inaccessible-but-extant PATH elements can't be executed anyway.
            if (pathAccessible(dir + "/nix-env"))
                dirs.insert(dirOf(canonPath(dir + "/nix-env", true)));

        if (dirs.size() != 1) {
            std::stringstream ss;
            ss << "Multiple versions of nix found in PATH:\n";
            for (auto & dir : dirs)
                ss << "  " << dir << "\n";
            return checkFail(ss.str());
        }

        return checkPass("PATH contains only one nix version.");
    }

    bool checkProfileRoots(ref<Store> store)
    {
        PathSet dirs;

        for (auto & dir : tokenizeString<Strings>(getEnv("PATH").value_or(""), ":")) {
            Path profileDir = dirOf(dir);
            try {
                Path userEnv = canonPath(profileDir, true);

                if (store->isStorePath(userEnv) && userEnv.ends_with("user-environment")) {
                    while (profileDir.find("/profiles/") == std::string::npos && isLink(profileDir))
                        profileDir = absPath(readLink(profileDir), dirOf(profileDir));

                    if (profileDir.find("/profiles/") == std::string::npos)
                        dirs.insert(dir);
                }
            } catch (SysError &) {}
        }

        if (!dirs.empty()) {
            std::stringstream ss;
            ss << "Found profiles outside of " << settings.nixStateDir << "/profiles.\n"
               << "The generation this profile points to might not have a gcroot and could be\n"
               << "garbage collected, resulting in broken symlinks.\n\n";
            for (auto & dir : dirs)
                ss << "  " << dir << "\n";
            ss << "\n";
            return checkFail(ss.str());
        }

        return checkPass("All profiles are gcroots.");
    }

    bool checkStoreProtocol(unsigned int storeProto)
    {
        unsigned int clientProto = GET_PROTOCOL_MAJOR(SERVE_PROTOCOL_VERSION) == GET_PROTOCOL_MAJOR(storeProto)
            ? SERVE_PROTOCOL_VERSION
            : PROTOCOL_VERSION;

        if (clientProto != storeProto) {
            std::stringstream ss;
            ss << "Warning: protocol version of this client does not match the store.\n"
               << "While this is not necessarily a problem it's recommended to keep the client in\n"
               << "sync with the daemon.\n\n"
               << "Client protocol: " << formatProtocol(clientProto) << "\n"
               << "Store protocol: " << formatProtocol(storeProto) << "\n\n";
            return checkFail(ss.str());
        }

        return checkPass("Client protocol matches store protocol.");
    }

    void checkTrustedUser(ref<Store> store)
    {
        auto trustedMay = aio().blockOn(store->isTrustedClient());
        std::string_view trustedness = trustedMay ? (*trustedMay ? "trusted" : "not trusted") : "unknown trust";
        checkInfo(fmt("You are %s by store uri: %s", trustedness, store->getUri()));
    }

    void checkValidCurrentProfileGeneration(const Path & profile)
    {
        Generations generations;
        std::optional<GenerationNumber> currentGeneration;
        std::string errStr;

        try {
            std::tie(generations, currentGeneration) = findGenerations(profile);
        } catch (SysError const & e) {
            errStr = fmt(": %s", e.msg());
        }

        if (!currentGeneration) {
            std::stringstream ss;
            ss << "Error: current generation cannot be discovered for profile: '" << profile << "'";
            ss << errStr << "\n";
            checkFail(ss.str());
        } else {
            std::stringstream ss;
            ss << "You have " << generations.size() << " generations for profile '" << profile << "'\n";
            ss << "The current generation number is '" << *currentGeneration << "'\n";
            checkPass(ss.str());
        }
    }
};

void registerNixDoctor()
{
    registerCommand<CmdDoctor>("doctor");
}

}