<commit_before>/*
UselessMine
    Copyright (C) 2013-2017 Vladimir "allejo" Jimenez

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program. If not, see <http://www.gnu.org/licenses/>.
 */

#include <algorithm>
#include <memory>
#include <stdlib.h>

#include "bzfsAPI.h"
#include "bztoolkit/bzToolkitAPI.h"

// Define plugin name
const std::string PLUGIN_NAME = "Useless Mine";

// Define plugin version numbering
const int MAJOR = 1;
const int MINOR = 1;
const int REV = 0;
const int BUILD = 52;

// A function to replace substrings in a string with another substring
std::string ReplaceString(std::string subject, const std::string& search, const std::string& replace)
{
    size_t pos = 0;

    while ((pos = subject.find(search, pos)) != std::string::npos)
    {
        subject.replace(pos, search.length(), replace);
        pos += replace.length();
    }

    return subject;
}

class UselessMine : public bz_Plugin, public bz_CustomSlashCommandHandler
{
public:
    virtual const char* Name ();
    virtual void Init (const char* config);
    virtual void Event (bz_EventData *eventData);
    virtual void Cleanup (void);

    virtual bool SlashCommand (int playerID, bz_ApiString, bz_ApiString, bz_APIStringList*);

    // The information each mine will contain
    struct Mine
    {
        bz_ApiString uid;      // A unique ID for mine

        int owner;             // The owner of the mine
        int defuserID;         // The player who defused this mine
        int detonationID;      // The shot ID of the world weapon that exploded when this mine was defused/detonated
        float x, y, z;         // The coordinates of where the mine was placed
        bz_eTeamType team;     // The team of the mine owner
        bool defused;          // True if the mine was defused with a Bomb Defusal flag
        bool detonated;        // True if the mine was detonated by a player
        double detonationTime; // The time the mine was detonated

        Mine (int _owner, float _pos[3], bz_eTeamType _team) :
            owner(_owner),
            defuserID(-1),
            detonationID(-1),
            x(_pos[0]),
            y(_pos[1]),
            z(_pos[2]),
            team(_team),
            defused(false),
            detonated(false),
            detonationTime(-1)
        {
            uid.format("%d_%d", owner, bz_getCurrentTime());
        }

        // Should a given player trigger this mine?
        // This function checks mine ownership, team loyalty, and player's alive-ness
        bool canPlayerTriggerMine(bz_BasePlayerRecord *pr, float pos[3])
        {
            if (owner != pr->playerID && (pr->team == eRogueTeam || pr->team != team || bz_getGameType() == eOpenFFAGame) && pr->spawned)
            {
                float  playerPos[3] = {pos[0], pos[1], pos[2]};
                double shockRange   = bz_getBZDBDouble("_shockOutRadius") * 0.75;

                // Check if the player is in the detonation range
                bool inDetonationRange = ((playerPos[0] > x - shockRange && playerPos[0] < x + shockRange) &&
                                          (playerPos[1] > y - shockRange && playerPos[1] < y + shockRange) &&
                                          (playerPos[2] > z - shockRange && playerPos[2] < z + shockRange));

                return inDetonationRange;
            }

            return false;
        }

        bool defuse(int playerID)
        {
            if (bztk_isValidPlayerID(owner) && bz_getPlayerTeam(owner) != eObservers)
            {
                bz_BasePlayerRecord *pr = bz_getPlayerByIndex(owner);

                bz_debugMessagef(4, "DEBUG :: Useless Mine :: Mine UID %s defused", uid.c_str());

                defused = true;
                defuserID = playerID;
                detonationTime = bz_getCurrentTime();

                bz_fireWorldWep("SW", 2.0, BZ_SERVER, pr->lastKnownState.pos, 0, 0, 0, &detonationID, 0, bz_getPlayerTeam(playerID));

                bz_freePlayerRecord(pr);

                return true;
            }

            return false;
        }

        bool detonate()
        {
            // Check that the mine owner exists and is not an observer
            if (bztk_isValidPlayerID(owner) && bz_getPlayerTeam(owner) != eObservers)
            {
                float minePos[3] = {x, y, z};

                // Only detonate a mine once
                if (!detonated)
                {
                    bz_debugMessagef(4, "DEBUG :: Useless Mine :: Mine UID %s detonated", uid.c_str());

                    detonated = true;
                    detonationTime = bz_getCurrentTime();

                    // BOOM!
                    bz_fireWorldWep("SW", 2.0, BZ_SERVER, minePos, 0, 0, 0, &detonationID, 0, team);

                    return true;
                }
            }

            return false;
        }
    };

private:
    virtual int         getMineCount (bool offset);
    virtual void        reloadDeathMessages (),
                        removePlayerMines (int playerID),
                        removeMine (Mine &mine),
                        setMine (int owner, float pos[3], bz_eTeamType team);
    virtual std::string formatDeathMessage (std::string msg, std::string victim, std::string owner);

    std::string deathMessagesFile;
    std::vector<std::string> deathMessages; // A vector that will store all of the witty death messages
    std::vector<Mine> activeMines; // A vector that will store all of the detonated mines queued for removal

    double playerSpawnTime[256];
};

BZ_PLUGIN(UselessMine)

const char* UselessMine::Name (void)
{
    static std::string pluginName;

    if (pluginName.empty())
        pluginName = bztk_pluginName(PLUGIN_NAME, MAJOR, MINOR, REV, BUILD);

    return pluginName.c_str();
}

void UselessMine::Init (const char* commandLine)
{
    Register(bz_eFlagGrabbedEvent);
    Register(bz_ePlayerDieEvent);
    Register(bz_ePlayerPartEvent);
    Register(bz_ePlayerSpawnEvent);
    Register(bz_ePlayerUpdateEvent);

    bz_registerCustomSlashCommand("mine", this);
    bz_registerCustomSlashCommand("reload", this);

    bz_RegisterCustomFlag("BD", "Bomb Defusal", "Safely defuse enemy mines while killing the mine owners", 0, eGoodFlag);

    bztk_registerCustomDoubleBZDB("_mineSafetyTime", 5.0);

    // Save the location of the file so we can reload after
    deathMessagesFile = commandLine;

    reloadDeathMessages();

    if (deathMessages.empty())
    {
        bz_debugMessage(2, "WARNING :: Useless Mine :: No witty death messages were loaded");
    }
    else
    {
        bz_debugMessagef(2, "DEBUG :: Useless Mine :: %d witty messages were loaded", deathMessages.size());
    }
}

void UselessMine::Cleanup (void)
{
    Flush();

    bz_removeCustomSlashCommand("mine");
    bz_removeCustomSlashCommand("reload");
}

void UselessMine::Event (bz_EventData *eventData)
{
    switch (eventData->eventType)
    {
        case bz_eFlagGrabbedEvent:
        {
            bz_FlagGrabbedEventData_V1* flagGrabData = (bz_FlagGrabbedEventData_V1*)eventData;

            // If the user grabbed the Useless flag, let them know they can place a mine
            if (strcmp(flagGrabData->flagType, "US") == 0)
            {
                bz_sendTextMessage(BZ_SERVER, flagGrabData->playerID, "You grabbed a Useless flag! Type /mine at any time to set a useless mine!");
            }
        }
        break;

        case bz_ePlayerDieEvent:
        {
            bz_PlayerDieEventData_V1* dieData = (bz_PlayerDieEventData_V1*)eventData;

            int playerID = dieData->playerID;

            for (Mine &mine : activeMines)
            {
                // This isn't the mine we're looking for since it hasn't been touched
                if (!mine.detonated && !mine.defused)
                {
                    continue;
                }

                // If the shot IDs don't match, this mine wasn't the killer
                if (dieData->shotID != mine.detonationID)
                {
                    continue;
                }

                // Get the callsigns of the players
                const char* owner  = bz_getPlayerCallsign(mine.owner);
                const char* victim = bz_getPlayerCallsign(playerID);

                if (mine.detonated)
                {
                    dieData->killerID = mine.owner;

                    if (!deathMessages.empty() && playerID != mine.owner)
                    {
                        // The random number used to fetch a random taunting death message
                        int randomNumber = rand() % deathMessages.size();

                        // Get a random death message
                        std::string deathMessage = deathMessages.at(randomNumber);
                        bz_sendTextMessage(BZ_SERVER, BZ_ALLUSERS, formatDeathMessage(deathMessage, victim, owner).c_str());
                    }
                }
                else if (mine.defused)
                {
                    dieData->killerID = mine.defuserID;

                    bz_sendTextMessagef(BZ_SERVER, BZ_ALLUSERS, "%s successfully defused %s's mine! [%d]", bz_getPlayerCallsign(mine.defuserID), owner, getMineCount(true));
                }

                removeMine(mine);

                break;
            }
        }
        break;

        case bz_ePlayerPartEvent:
        {
            bz_PlayerJoinPartEventData_V1* partData = (bz_PlayerJoinPartEventData_V1*)eventData;

            int playerID = partData->playerID;

            // Remove all the mines belonging to the player who just left
            removePlayerMines(playerID);
            playerSpawnTime[playerID] = -1;
        }
        break;

        case bz_ePlayerSpawnEvent:
        {
            bz_PlayerSpawnEventData_V1* spawnData = (bz_PlayerSpawnEventData_V1*)eventData;

            int playerID = spawnData->playerID;

            // Save the time the player spawned last
            playerSpawnTime[playerID] = bz_getCurrentTime();
        }
        break;

        case bz_ePlayerUpdateEvent:
        {
            bz_PlayerUpdateEventData_V1* updateData = (bz_PlayerUpdateEventData_V1*)eventData;

            int playerID = updateData->playerID;
            bz_BasePlayerRecord *pr = bz_getPlayerByIndex(playerID);

            for (Mine &mine : activeMines)
            {
                bool bypassSafetyTime = (playerSpawnTime[pr->playerID] + bz_getBZDBDouble("_mineSafetyTime") <= bz_getCurrentTime());

                if (mine.canPlayerTriggerMine(pr, updateData->state.pos) && bypassSafetyTime)
                {
                    (pr->currentFlag == "Bomb Defusal (+BD)") ? mine.defuse(playerID) : mine.detonate();
                }
            }

            bz_freePlayerRecord(pr);
        }
        break;

        default:
            break;
    }
}

bool UselessMine::SlashCommand(int playerID, bz_ApiString command, bz_ApiString /*message*/, bz_APIStringList *params)
{
    if (command == "mine")
    {
        std::unique_ptr<bz_BasePlayerRecord> playerRecord(bz_getPlayerByIndex(playerID));

        if (playerRecord->team != eObservers)
        {
            // Check if the player has the Useless flag
            if (playerRecord->currentFlag == "USeless (+US)")
            {
                // Store their current position
                float currentPosition[3] = {playerRecord->lastKnownState.pos[0], playerRecord->lastKnownState.pos[1], playerRecord->lastKnownState.pos[2]};

                setMine(playerID, currentPosition, playerRecord->team);
            }
            else
            {
                bz_sendTextMessage(BZ_SERVER, playerID, "You can't place a mine without the Useless flag!");
            }
        }
        else
        {
            bz_sendTextMessage(BZ_SERVER, playerID, "Silly observer, you can't place a mine.");
        }

        return true;
    }
    else if (command == "reload" && bz_hasPerm(playerID, "setAll"))
    {
        if (params->get(0) == "deathmessages")
        {
            reloadDeathMessages();
            bz_sendTextMessage(BZ_SERVER, playerID, "Death messages reloaded");
            return true;
        }
        else if (params->size() == 0)
        {
            reloadDeathMessages();
        }
    }

    return false;
}

// A function to format death messages in order to replace placeholders with callsigns and values
std::string UselessMine::formatDeathMessage(std::string msg, std::string victim, std::string owner)
{
    // Replace the %victim% and %owner% placeholders
    std::string formattedMessage = ReplaceString(ReplaceString(msg, "%victim%", victim), "%owner%", owner);

    // If the message has a %minecount%, then replace it
    if (formattedMessage.find("%minecount%") != std::string::npos)
    {
        formattedMessage = ReplaceString(formattedMessage, "%minecount%", std::to_string(getMineCount(true)));
    }

    return formattedMessage;
}

void UselessMine::reloadDeathMessages()
{
    deathMessages.clear();

    if (!deathMessagesFile.empty())
    {
        bztk_fileToVector(deathMessagesFile.c_str(), deathMessages);
    }
}

// Get the amount of active mines that exist
// Setting `offset` to true will subtract the current detonated/defused mine that's queued for removal
int UselessMine::getMineCount(bool offset = false)
{
    if (offset && activeMines.size() == 0)
    {
        return 0;
    }

    return (int)activeMines.size() - (offset ? 1 : 0);
}

// Remove a specific mine
void UselessMine::removeMine(Mine &mine)
{
    const char* uid = mine.uid.c_str();

    bz_debugMessagef(4, "DEBUG :: Useless Mine :: Removing mine UID: %s", mine.uid.c_str());
    bz_debugMessagef(4, "DEBUG :: Useless Mine ::  mine count: %d", getMineCount());

    activeMines.erase(
        std::remove_if(
            activeMines.begin(),
            activeMines.end(),
            [uid](Mine &m) { return m.uid == uid; }
        ),
        activeMines.end()
    );

    bz_debugMessagef(4, "DEBUG :: Useless Mine ::  new mine count: %d", getMineCount());
}

// Remove all of the mines of a specific player
void UselessMine::removePlayerMines(int playerID)
{
    activeMines.erase(
        std::remove_if(
            activeMines.begin(),
            activeMines.end(),
            [playerID](Mine &m) { return m.owner == playerID; }
        ),
        activeMines.end()
    );
}

// A shortcut to set a mine
void UselessMine::setMine(int owner, float pos[3], bz_eTeamType team)
{
    // Remove their flag because it's going to be a US flag
    bz_removePlayerFlag(owner);

    // Push the new mine
    Mine newMine(owner, pos, team);
    activeMines.push_back(newMine);
}
<commit_msg>Clean-up + more debug messages<commit_after>/*
UselessMine
    Copyright (C) 2013-2017 Vladimir "allejo" Jimenez

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program. If not, see <http://www.gnu.org/licenses/>.
 */

#include <algorithm>
#include <memory>
#include <stdlib.h>

#include "bzfsAPI.h"
#include "bztoolkit/bzToolkitAPI.h"

// Define plugin name
const std::string PLUGIN_NAME = "Useless Mine";

// Define plugin version numbering
const int MAJOR = 1;
const int MINOR = 1;
const int REV = 0;
const int BUILD = 52;

// A function to replace substrings in a string with another substring
std::string ReplaceString(std::string subject, const std::string& search, const std::string& replace)
{
    size_t pos = 0;

    while ((pos = subject.find(search, pos)) != std::string::npos)
    {
        subject.replace(pos, search.length(), replace);
        pos += replace.length();
    }

    return subject;
}

class UselessMine : public bz_Plugin, public bz_CustomSlashCommandHandler
{
public:
    virtual const char* Name ();
    virtual void Init (const char* config);
    virtual void Event (bz_EventData *eventData);
    virtual void Cleanup (void);

    virtual bool SlashCommand (int playerID, bz_ApiString, bz_ApiString, bz_APIStringList*);

    // The information each mine will contain
    struct Mine
    {
        bz_ApiString uid;      // A unique ID for mine

        int owner;             // The owner of the mine
        int defuserID;         // The player who defused this mine
        int detonationID;      // The shot ID of the world weapon that exploded when this mine was defused/detonated
        float x, y, z;         // The coordinates of where the mine was placed
        bz_eTeamType team;     // The team of the mine owner
        bool defused;          // True if the mine was defused with a Bomb Defusal flag
        bool detonated;        // True if the mine was detonated by a player
        double detonationTime; // The time the mine was detonated

        Mine (int _owner, float _pos[3], bz_eTeamType _team) :
            owner(_owner),
            defuserID(-1),
            detonationID(-1),
            x(_pos[0]),
            y(_pos[1]),
            z(_pos[2]),
            team(_team),
            defused(false),
            detonated(false),
            detonationTime(-1)
        {
            uid.format("%d_%d", owner, bz_getCurrentTime());
        }

        // Should a given player trigger this mine?
        // This function checks mine ownership, team loyalty, and player's alive-ness
        bool canPlayerTriggerMine(bz_BasePlayerRecord *pr, float pos[3])
        {
            if (owner != pr->playerID && (pr->team == eRogueTeam || pr->team != team || bz_getGameType() == eOpenFFAGame) && pr->spawned)
            {
                float  playerPos[3] = {pos[0], pos[1], pos[2]};
                double shockRange   = bz_getBZDBDouble("_shockOutRadius") * 0.75;

                // Check if the player is in the detonation range
                bool inDetonationRange = ((playerPos[0] > x - shockRange && playerPos[0] < x + shockRange) &&
                                          (playerPos[1] > y - shockRange && playerPos[1] < y + shockRange) &&
                                          (playerPos[2] > z - shockRange && playerPos[2] < z + shockRange));

                return inDetonationRange;
            }

            return false;
        }

        bool defuse(int playerID)
        {
            if (bztk_isValidPlayerID(owner) && bz_getPlayerTeam(owner) != eObservers)
            {
                bz_BasePlayerRecord *pr = bz_getPlayerByIndex(owner);

                bz_debugMessagef(4, "DEBUG :: Useless Mine :: Mine UID %s defused", uid.c_str());

                defused = true;
                defuserID = playerID;
                detonationTime = bz_getCurrentTime();

                bz_fireWorldWep("SW", 2.0, BZ_SERVER, pr->lastKnownState.pos, 0, 0, 0, &detonationID, 0, bz_getPlayerTeam(playerID));

                bz_freePlayerRecord(pr);

                return true;
            }

            return false;
        }

        bool detonate()
        {
            // Check that the mine owner exists and is not an observer
            if (bztk_isValidPlayerID(owner) && bz_getPlayerTeam(owner) != eObservers)
            {
                float minePos[3] = {x, y, z};

                // Only detonate a mine once
                if (!detonated)
                {
                    bz_debugMessagef(4, "DEBUG :: Useless Mine :: Mine UID %s detonated", uid.c_str());

                    detonated = true;
                    detonationTime = bz_getCurrentTime();

                    bz_fireWorldWep("SW", 2.0, BZ_SERVER, minePos, 0, 0, 0, &detonationID, 0, team);

                    return true;
                }
            }

            return false;
        }
    };

private:
    int  getMineCount (bool offset);
    void reloadDeathMessages (),
         removePlayerMines (int playerID),
         removeMine (Mine &mine),
         setMine (int owner, float pos[3], bz_eTeamType team);
    std::string formatDeathMessage (std::string msg, std::string victim, std::string owner);

    std::vector<std::string> deathMessages; // A vector that will store all of the witty death messages
    std::vector<Mine> activeMines; // A vector that will store all of the mines currently on the field
    std::string deathMessagesFile; // The path to the file containing death messages

    double playerSpawnTime[256]; // The time a player spawned last; used for _mineSafetyTime calculations
};

BZ_PLUGIN(UselessMine)

const char* UselessMine::Name (void)
{
    static std::string pluginName;

    if (pluginName.empty())
        pluginName = bztk_pluginName(PLUGIN_NAME, MAJOR, MINOR, REV, BUILD);

    return pluginName.c_str();
}

void UselessMine::Init (const char* commandLine)
{
    Register(bz_eFlagGrabbedEvent);
    Register(bz_ePlayerDieEvent);
    Register(bz_ePlayerPartEvent);
    Register(bz_ePlayerSpawnEvent);
    Register(bz_ePlayerUpdateEvent);

    bz_registerCustomSlashCommand("mine", this);
    bz_registerCustomSlashCommand("reload", this);

    bz_RegisterCustomFlag("BD", "Bomb Defusal", "Safely defuse enemy mines while killing the mine owners", 0, eGoodFlag);

    bztk_registerCustomDoubleBZDB("_mineSafetyTime", 5.0);

    // Save the location of the file so we can reload after
    deathMessagesFile = commandLine;

    reloadDeathMessages();

    if (deathMessages.empty())
    {
        bz_debugMessage(2, "WARNING :: Useless Mine :: No witty death messages were loaded");
    }
    else
    {
        bz_debugMessagef(2, "DEBUG :: Useless Mine :: %d witty messages were loaded", deathMessages.size());
    }
}

void UselessMine::Cleanup (void)
{
    Flush();

    bz_removeCustomSlashCommand("mine");
    bz_removeCustomSlashCommand("reload");
}

void UselessMine::Event (bz_EventData *eventData)
{
    switch (eventData->eventType)
    {
        case bz_eFlagGrabbedEvent:
        {
            bz_FlagGrabbedEventData_V1* flagGrabData = (bz_FlagGrabbedEventData_V1*)eventData;

            // If the user grabbed the Useless flag, let them know they can place a mine
            if (strcmp(flagGrabData->flagType, "US") == 0)
            {
                bz_sendTextMessage(BZ_SERVER, flagGrabData->playerID, "You grabbed a Useless flag! Type /mine at any time to set a useless mine!");
            }
        }
        break;

        case bz_ePlayerDieEvent:
        {
            bz_PlayerDieEventData_V1* dieData = (bz_PlayerDieEventData_V1*)eventData;

            int playerID = dieData->playerID;

            for (Mine &mine : activeMines)
            {
                // This isn't the mine we're looking for since it hasn't been touched
                if (!mine.detonated && !mine.defused)
                {
                    continue;
                }

                // If the shot IDs don't match, this mine wasn't the killer
                if (dieData->shotID != mine.detonationID)
                {
                    continue;
                }

                // Get the callsigns of the players
                const char* owner  = bz_getPlayerCallsign(mine.owner);
                const char* victim = bz_getPlayerCallsign(playerID);

                if (mine.detonated)
                {
                    dieData->killerID = mine.owner;

                    if (!deathMessages.empty() && playerID != mine.owner)
                    {
                        // The random number used to fetch a random taunting death message
                        int randomNumber = rand() % deathMessages.size();

                        // Get a random death message
                        std::string deathMessage = deathMessages.at(randomNumber);
                        bz_sendTextMessage(BZ_SERVER, BZ_ALLUSERS, formatDeathMessage(deathMessage, victim, owner).c_str());
                    }
                }
                else if (mine.defused)
                {
                    dieData->killerID = mine.defuserID;

                    bz_sendTextMessagef(BZ_SERVER, BZ_ALLUSERS, "%s successfully defused %s's mine! [%d]", bz_getPlayerCallsign(mine.defuserID), owner, getMineCount(true));
                }

                removeMine(mine);

                break;
            }
        }
        break;

        case bz_ePlayerPartEvent:
        {
            bz_PlayerJoinPartEventData_V1* partData = (bz_PlayerJoinPartEventData_V1*)eventData;

            int playerID = partData->playerID;

            // Remove all the mines belonging to the player who just left
            removePlayerMines(playerID);
            playerSpawnTime[playerID] = -1;
        }
        break;

        case bz_ePlayerSpawnEvent:
        {
            bz_PlayerSpawnEventData_V1* spawnData = (bz_PlayerSpawnEventData_V1*)eventData;

            int playerID = spawnData->playerID;

            // Save the time the player spawned last
            playerSpawnTime[playerID] = bz_getCurrentTime();
        }
        break;

        case bz_ePlayerUpdateEvent:
        {
            bz_PlayerUpdateEventData_V1* updateData = (bz_PlayerUpdateEventData_V1*)eventData;

            int playerID = updateData->playerID;
            bz_BasePlayerRecord *pr = bz_getPlayerByIndex(playerID);

            for (Mine &mine : activeMines)
            {
                bool bypassSafetyTime = (playerSpawnTime[pr->playerID] + bz_getBZDBDouble("_mineSafetyTime") <= bz_getCurrentTime());

                if (mine.canPlayerTriggerMine(pr, updateData->state.pos) && bypassSafetyTime)
                {
                    (pr->currentFlag == "Bomb Defusal (+BD)") ? mine.defuse(playerID) : mine.detonate();
                }
            }

            bz_freePlayerRecord(pr);
        }
        break;

        default:
            break;
    }
}

bool UselessMine::SlashCommand(int playerID, bz_ApiString command, bz_ApiString /*message*/, bz_APIStringList *params)
{
    if (command == "mine")
    {
        bz_BasePlayerRecord *pr = bz_getPlayerByIndex(playerID);

        if (pr->team != eObservers)
        {
            // Check if the player has the Useless flag
            if (pr->currentFlag == "USeless (+US)")
            {
                // Store their current position
                float currentPosition[3] = {pr->lastKnownState.pos[0], pr->lastKnownState.pos[1], pr->lastKnownState.pos[2]};

                setMine(playerID, currentPosition, pr->team);
            }
            else
            {
                bz_sendTextMessage(BZ_SERVER, playerID, "You can't place a mine without the Useless flag!");
            }
        }
        else
        {
            bz_sendTextMessage(BZ_SERVER, playerID, "Silly observer, you can't place a mine.");
        }

        bz_freePlayerRecord(pr);

        return true;
    }
    else if (command == "reload" && bz_hasPerm(playerID, "setAll"))
    {
        if (params->get(0) == "deathmessages")
        {
            reloadDeathMessages();
            bz_sendTextMessage(BZ_SERVER, playerID, "Death messages reloaded");
            return true;
        }
        else if (params->size() == 0)
        {
            reloadDeathMessages();
        }
    }

    return false;
}

// A function to format death messages in order to replace placeholders with callsigns and values
std::string UselessMine::formatDeathMessage(std::string msg, std::string victim, std::string owner)
{
    // Replace the %victim% and %owner% placeholders
    std::string formattedMessage = ReplaceString(ReplaceString(msg, "%victim%", victim), "%owner%", owner);

    // If the message has a %minecount%, then replace it
    if (formattedMessage.find("%minecount%") != std::string::npos)
    {
        formattedMessage = ReplaceString(formattedMessage, "%minecount%", std::to_string(getMineCount(true)));
    }

    return formattedMessage;
}

void UselessMine::reloadDeathMessages()
{
    deathMessages.clear();

    if (!deathMessagesFile.empty())
    {
        bztk_fileToVector(deathMessagesFile.c_str(), deathMessages);
    }
}

// Get the amount of active mines that exist
// Setting `offset` to true will subtract the current detonated/defused mine that's queued for removal
int UselessMine::getMineCount(bool offset = false)
{
    if (offset && activeMines.size() == 0)
    {
        return 0;
    }

    return (int)activeMines.size() - (offset ? 1 : 0);
}

// Remove a specific mine
void UselessMine::removeMine(Mine &mine)
{
    const char* uid = mine.uid.c_str();

    bz_debugMessagef(4, "DEBUG :: Useless Mine :: Removing mine UID: %s", mine.uid.c_str());
    bz_debugMessagef(4, "DEBUG :: Useless Mine ::  mine count: %d", getMineCount());

    activeMines.erase(
        std::remove_if(
            activeMines.begin(),
            activeMines.end(),
            [uid](Mine &m) { return m.uid == uid; }
        ),
        activeMines.end()
    );

    bz_debugMessagef(4, "DEBUG :: Useless Mine ::  new mine count: %d", getMineCount());
}

// Remove all of the mines of a specific player
void UselessMine::removePlayerMines(int playerID)
{
    bz_debugMessagef(4, "DEBUG :: Useless Mine :: Removing all mines for player %d", playerID);

    activeMines.erase(
        std::remove_if(
            activeMines.begin(),
            activeMines.end(),
            [playerID](Mine &m) { return m.owner == playerID; }
        ),
        activeMines.end()
    );
}

// A shortcut to set a mine
void UselessMine::setMine(int owner, float pos[3], bz_eTeamType team)
{
    // Remove their flag because they "converted" it into a mine
    bz_removePlayerFlag(owner);

    Mine newMine(owner, pos, team);

    bz_debugMessagef(4, "DEBUG :: Useless Mine :: Mine UID %s created by %d", newMine.uid.c_str(), owner);
    bz_debugMessagef(4, "DEBUG :: Useless Mine ::   x, y, z => %0.2f, %0.2f, %0.2f", pos[0], pos[1], pos[2]);

    activeMines.push_back(newMine);
}
<|endoftext|>


<commit_before>#include "mainwindow.h"
#include "ui_mainwindow.h"
#include "loadingwindow.h"
#include "projectproperties.h"
#include "volumeproperties.h"
#include <QAction>
#include <QMessageBox>
#include <QJsonDocument>
#include <QStandardItemModel>

MainWindow::MainWindow(QScanlateServer *server, QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    this->scanlate = new QScanlate(server, this);

    m_login_label = new QLabel(this->scanlate->getUser()->getLogin());
    ui->statusBar->addWidget(m_login_label);
    m_network_label = new QLabel((this->scanlate->getMode() == QScanlateServer::NORNAL) ? "online" : "offline");
    ui->statusBar->addWidget(m_network_label);
    m_project_label = new QLabel("");
    ui->statusBar->addWidget(m_project_label);
    m_chapter_label = new QLabel("");
    ui->statusBar->addWidget(m_chapter_label);

    ui->twChapters->setEnabled(false);

    LoadingWindow loadingWindow(this);
    connect(&loadingWindow, SIGNAL(loadingProc()), this, SLOT(LoadProjectsList()));
    connect(this, SIGNAL(UpdateProgress(QString,int,int)), &loadingWindow, SLOT(UpdateProgress(QString,int,int)));
    loadingWindow.Start();
    loadingWindow.exec();
}

MainWindow::~MainWindow()
{
    delete this->scanlate;
    delete ui;
}

void MainWindow::setUser(QString login, QString token)
{
    m_logged_as = login;
    m_token = token;
    m_login_label->setText(login);
}

#define LOADING_STEPS 4

void MainWindow::LoadProjectsList()
{
    emit UpdateProgress(QObject::tr("Сведения о пользователе"), LOADING_STEPS, 1);
    scanlate->getUserInfo();
    ui->lLogin->setText(this->scanlate->getUser()->getLogin());

    emit UpdateProgress(QObject::tr("Список участников"), LOADING_STEPS, 2);
    scanlate->getUsersList();

    emit UpdateProgress(QObject::tr("Список проектов"), LOADING_STEPS, 3);
    scanlate->getProjectsList(ui->twProjects);

    emit UpdateProgress("", LOADING_STEPS, LOADING_STEPS + 1);
}

void MainWindow::UpdateProjectInfo(QScanlateProject *project)
{
    if (project->getId() != -1)
        this->scanlate->UpdateProjectInfo(project);
    else
        this->scanlate->addNewProject(project, ui->twProjects);
}

void MainWindow::UpdateVolumeInfo(QVolume *volume)
{
    if (volume->getId() != -1)
        this->scanlate->updateVolumeInfo(volume);
    else
        this->scanlate->addNewVolume(volume, this->ui->twChapters);
}

void MainWindow::setEnabledMenu(const QMenu &menu)
{
    QList<QAction*>actions = menu.actions();
    foreach (QAction* action, actions)
    {
        QString actionName = action->data().toString();
        if ((actionName.startsWith("add")) || (actionName.startsWith("del"))|| (actionName.startsWith("edit")))
        {
            action->setEnabled(this->scanlate->getMode() == QScanlateServer::NORNAL);
        }
    }
}

void MainWindow::addNewProjectDialog()
{
    QScanlateProject *project = new QScanlateProject();
    project->deserialize(QJsonDocument::fromJson("{\"id\":-1}").object());
    if (projectPropertiesDialog(project))
        this->scanlate->addNewProject(project, this->ui->twProjects);
}

bool MainWindow::projectPropertiesDialog(QScanlateProject *project)
{
    ProjectProperties properties(project, this->scanlate->getUser()->isModerator(), this);
    connect(&properties, SIGNAL(UpdateProjectInfo(QScanlateProject*)),
            this, SLOT(UpdateProjectInfo(QScanlateProject*)));
    properties.show();
    return (properties.result() == QDialog::DialogCode::Accepted);
}

void MainWindow::setActiveProject(QScanlateProject *project)
{
    this->scanlate->setActiveProject(project);
    m_project_label->setText(QObject::tr("Активный проект: ") + project->getName());
    this->scanlate->getChaptersList(project, ui->twChapters);
    ui->twChapters->setEnabled(true);
    ui->tabWidget->setCurrentIndex(ui->tabWidget->indexOf(ui->tab_chapters));
}

void MainWindow::deleteProject(QScanlateProject *project)
{
    this->scanlate->deleteProject(project);
}

void MainWindow::addNewVolumeDialog()
{
    QVolume *volume = new QVolume();
    volume->deserialize(QJsonDocument::fromJson("{\"id\":-1}").object());
    if (volumePropertiesDialog(volume))
        this->scanlate->addNewVolume(volume, this->ui->twChapters);
}

bool MainWindow::volumePropertiesDialog(QVolume *volume, QChapter *chapter)
{
    VolumeProperties properties(volume, chapter, this);
    connect(&properties, SIGNAL(UpdateVolumeInfo(QVolume*)),
            this, SLOT(UpdateVolumeInfo(QVolume*,)));
    properties.show();
    return (properties.result() == QDialog::DialogCode::Accepted);
}

void MainWindow::deleteVolume(QVolume *volume)
{
    // TODO
}

void MainWindow::addNewChapterDialog(QVolume *volume)
{
    QChapter *chapter = new QChapter();
    chapter->deserialize(QJsonDocument::fromJson("{\"id\":-1}").object());
    //if (volumePropertiesDialog(volume, chapter))
        //
}

void MainWindow::setActiveChapter(QChapter *chapter)
{
    this->m_chapter_label->setText(QObject::tr("Глава: ") + QString::number(chapter->getNumber()));
    // TODO
}

void MainWindow::deleteChapter(QVolume *volume, QChapter *chapter)
{
    // TODO
}

void MainWindow::on_twProjects_doubleClicked(const QModelIndex &index)
{
    int project_id = ui->twProjects->item(index.row(), 0)->data(Qt::UserRole).toInt();
    if (project_id == -1)
    {
        if (this->scanlate->getMode() == QScanlateServer::NORNAL)
        {
            addNewProjectDialog();
        }
        else
        {
            QMessageBox::critical(this, QObject::tr("Ошибка"),
                                    QObject::tr("Данный функционал доступен только в онлайн-режиме!"));
            return;
        }
    }
    else
        projectPropertiesDialog(scanlate->getProjectByID(project_id));
}

void MainWindow::on_twProjects_customContextMenuRequested(const QPoint &pos)
{
    QMenu menu;
    int project_id = ui->twProjects->item(ui->twProjects->currentRow(), 0)->data(Qt::UserRole).toInt();
    if (project_id == -1)
    {
        menu.addAction(QObject::tr("&Добавить проект"))->setData("addProject");
    }
    else
    {
        menu.addAction(QObject::tr("&Свойства"))->setData("propertiesProject");
        menu.addAction(QObject::tr("&Выбрать активным"))->setData("setActiveProject");
        menu.addSeparator();
        menu.addAction(QObject::tr("&Добавить проект"))->setData("addProject");
        menu.addAction(QObject::tr("&Удалить"))->setData("delProject");
    }
    this->setEnabledMenu(menu);

    QAction* action;
    if (pos != QPoint(0,0))
        action =  menu.exec(ui->twProjects->viewport()->mapToGlobal(pos));

    QScanlateProject *project = this->scanlate->getProjectByID(project_id);
    if(!action)
        return;
    else if (action->data().toString() == "setActiveProject")
        setActiveProject(project);
    else if (action->data().toString() == "propertiesProject")
        projectPropertiesDialog(project);
    else if (action->data().toString() == "addProject")
        addNewProjectDialog();
    else if (action->data().toString() == "delProject")
        deleteProject(project);
}

void MainWindow::on_twChapters_customContextMenuRequested(const QPoint &pos)
{
    QMenu menu;
    int volume_id, chapter_id;

    if (!ui->twChapters->currentItem()->parent())
    {
        // selected root item - volume
        volume_id = ui->twChapters->currentItem()->data(0, Qt::UserRole).toInt();
        chapter_id = -1;
        menu.addAction(QObject::tr("&Добавить главу"))->setData("addChapter");
        menu.addAction(QObject::tr("&Добавить том"))->setData("addVolume");
        menu.addAction(QObject::tr("&Редактировать"))->setData("editVolume");
        menu.addSeparator();
        menu.addAction(QObject::tr("&Удалить"))->setData("delVolume");
    }
    else
    {
        // selected children item - chapter
        volume_id = ui->twChapters->currentItem()->parent()->data(0, Qt::UserRole).toInt();
        chapter_id = ui->twChapters->currentItem()->data(0, Qt::UserRole).toInt();

        menu.addAction(QObject::tr("&Выбрать активной"))->setData("setActive");
        menu.addAction(QObject::tr("&Добавить главу"))->setData("addChapterEx");
        menu.addAction(QObject::tr("&Редактировать"))->setData("editVolume");
        menu.addSeparator();
        menu.addAction(QObject::tr("&Удалить"))->setData("delChapter");
    }
    this->setEnabledMenu(menu);

    QVolume *volume = this->scanlate->getActiveProject()->getVolumeById(volume_id);
    QChapter *chapter = volume->getChapterById(chapter_id);

    QAction* action;
    if (pos != QPoint(0,0))
        action =  menu.exec(ui->twChapters->viewport()->mapToGlobal(pos));

    if(!action)
        return;
    else if (action->data().toString() == "setActive")
        setActiveChapter(chapter);
    else if ((action->data().toString() == "addChapter") || (action->data().toString() == "addChapterEx"))
        addNewChapterDialog(volume);
    else if (action->data().toString() == "addVolume")
        addNewVolumeDialog();
    else if (action->data().toString() == "editVolume")
        volumePropertiesDialog(volume);
    else if (action->data().toString() == "delVolume")
        deleteVolume(volume);
    else if (action->data().toString() == "delChapter")
        deleteChapter(volume, chapter);
}

void MainWindow::saveState()
{
    scanlate->saveState("./data/");
}

void MainWindow::on_MainWindow_destroyed()
{
    scanlate->saveState("./data/");
}
<commit_msg>Убрано неопределенное поведение в случае ошибки.<commit_after>#include "mainwindow.h"
#include "ui_mainwindow.h"
#include "loadingwindow.h"
#include "projectproperties.h"
#include "volumeproperties.h"
#include <QAction>
#include <QMessageBox>
#include <QJsonDocument>
#include <QStandardItemModel>

MainWindow::MainWindow(QScanlateServer *server, QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    this->scanlate = new QScanlate(server, this);

    m_login_label = new QLabel(this->scanlate->getUser()->getLogin());
    ui->statusBar->addWidget(m_login_label);
    m_network_label = new QLabel((this->scanlate->getMode() == QScanlateServer::NORNAL) ? "online" : "offline");
    ui->statusBar->addWidget(m_network_label);
    m_project_label = new QLabel("");
    ui->statusBar->addWidget(m_project_label);
    m_chapter_label = new QLabel("");
    ui->statusBar->addWidget(m_chapter_label);

    ui->twChapters->setEnabled(false);

    LoadingWindow loadingWindow(this);
    connect(&loadingWindow, SIGNAL(loadingProc()), this, SLOT(LoadProjectsList()));
    connect(this, SIGNAL(UpdateProgress(QString,int,int)), &loadingWindow, SLOT(UpdateProgress(QString,int,int)));
    loadingWindow.Start();
    loadingWindow.exec();
}

MainWindow::~MainWindow()
{
    delete this->scanlate;
    delete ui;
}

void MainWindow::setUser(QString login, QString token)
{
    m_logged_as = login;
    m_token = token;
    m_login_label->setText(login);
}

#define LOADING_STEPS 4

void MainWindow::LoadProjectsList()
{
    emit UpdateProgress(QObject::tr("Сведения о пользователе"), LOADING_STEPS, 1);
    scanlate->getUserInfo();
    ui->lLogin->setText(this->scanlate->getUser()->getLogin());

    emit UpdateProgress(QObject::tr("Список участников"), LOADING_STEPS, 2);
    scanlate->getUsersList();

    emit UpdateProgress(QObject::tr("Список проектов"), LOADING_STEPS, 3);
    scanlate->getProjectsList(ui->twProjects);

    emit UpdateProgress("", LOADING_STEPS, LOADING_STEPS + 1);
}

void MainWindow::UpdateProjectInfo(QScanlateProject *project)
{
    if (project->getId() != -1)
        this->scanlate->UpdateProjectInfo(project);
    else
        this->scanlate->addNewProject(project, ui->twProjects);
}

void MainWindow::UpdateVolumeInfo(QVolume *volume)
{
    if (volume->getId() != -1)
        this->scanlate->updateVolumeInfo(volume);
    else
        this->scanlate->addNewVolume(volume, this->ui->twChapters);
}

void MainWindow::setEnabledMenu(const QMenu &menu)
{
    QList<QAction*>actions = menu.actions();
    foreach (QAction* action, actions)
    {
        QString actionName = action->data().toString();
        if ((actionName.startsWith("add")) || (actionName.startsWith("del"))|| (actionName.startsWith("edit")))
        {
            action->setEnabled(this->scanlate->getMode() == QScanlateServer::NORNAL);
        }
    }
}

void MainWindow::addNewProjectDialog()
{
    QScanlateProject *project = new QScanlateProject();
    project->deserialize(QJsonDocument::fromJson("{\"id\":-1}").object());
    if (projectPropertiesDialog(project))
        this->scanlate->addNewProject(project, this->ui->twProjects);
}

bool MainWindow::projectPropertiesDialog(QScanlateProject *project)
{
    ProjectProperties properties(project, this->scanlate->getUser()->isModerator(), this);
    connect(&properties, SIGNAL(UpdateProjectInfo(QScanlateProject*)),
            this, SLOT(UpdateProjectInfo(QScanlateProject*)));
    properties.show();
    return (properties.result() == QDialog::DialogCode::Accepted);
}

void MainWindow::setActiveProject(QScanlateProject *project)
{
    this->scanlate->setActiveProject(project);
    m_project_label->setText(QObject::tr("Активный проект: ") + project->getName());
    this->scanlate->getChaptersList(project, ui->twChapters);
    ui->twChapters->setEnabled(true);
    ui->tabWidget->setCurrentIndex(ui->tabWidget->indexOf(ui->tab_chapters));
}

void MainWindow::deleteProject(QScanlateProject *project)
{
    this->scanlate->deleteProject(project);
}

void MainWindow::addNewVolumeDialog()
{
    QVolume *volume = new QVolume();
    volume->deserialize(QJsonDocument::fromJson("{\"id\":-1}").object());
    if (volumePropertiesDialog(volume))
        this->scanlate->addNewVolume(volume, this->ui->twChapters);
}

bool MainWindow::volumePropertiesDialog(QVolume *volume, QChapter *chapter)
{
    VolumeProperties properties(volume, chapter, this);
    connect(&properties, SIGNAL(UpdateVolumeInfo(QVolume*)),
            this, SLOT(UpdateVolumeInfo(QVolume*,)));
    properties.show();
    return (properties.result() == QDialog::DialogCode::Accepted);
}

void MainWindow::deleteVolume(QVolume *volume)
{
    // TODO
}

void MainWindow::addNewChapterDialog(QVolume *volume)
{
    QChapter *chapter = new QChapter();
    chapter->deserialize(QJsonDocument::fromJson("{\"id\":-1}").object());
    //if (volumePropertiesDialog(volume, chapter))
        //
}

void MainWindow::setActiveChapter(QChapter *chapter)
{
    this->m_chapter_label->setText(QObject::tr("Глава: ") + QString::number(chapter->getNumber()));
    // TODO
}

void MainWindow::deleteChapter(QVolume *volume, QChapter *chapter)
{
    // TODO
}

void MainWindow::on_twProjects_doubleClicked(const QModelIndex &index)
{
    int project_id = ui->twProjects->item(index.row(), 0)->data(Qt::UserRole).toInt();
    if (project_id == -1)
    {
        if (this->scanlate->getMode() == QScanlateServer::NORNAL)
        {
            addNewProjectDialog();
        }
        else
        {
            QMessageBox::critical(this, QObject::tr("Ошибка"),
                                    QObject::tr("Данный функционал доступен только в онлайн-режиме!"));
            return;
        }
    }
    else
        projectPropertiesDialog(scanlate->getProjectByID(project_id));
}

void MainWindow::on_twProjects_customContextMenuRequested(const QPoint &pos)
{
    QMenu menu;
    int project_id = ui->twProjects->item(ui->twProjects->currentRow(), 0)->data(Qt::UserRole).toInt();
    if (project_id == -1)
    {
        menu.addAction(QObject::tr("&Добавить проект"))->setData("addProject");
    }
    else
    {
        menu.addAction(QObject::tr("&Свойства"))->setData("propertiesProject");
        menu.addAction(QObject::tr("&Выбрать активным"))->setData("setActiveProject");
        menu.addSeparator();
        menu.addAction(QObject::tr("&Добавить проект"))->setData("addProject");
        menu.addAction(QObject::tr("&Удалить"))->setData("delProject");
    }
    this->setEnabledMenu(menu);

    QAction* action;
    if (pos != QPoint(0,0))
        action =  menu.exec(ui->twProjects->viewport()->mapToGlobal(pos));
    else
        return;

    QScanlateProject *project = this->scanlate->getProjectByID(project_id);
    if(!action)
        return;
    else if (action->data().toString() == "setActiveProject")
        setActiveProject(project);
    else if (action->data().toString() == "propertiesProject")
        projectPropertiesDialog(project);
    else if (action->data().toString() == "addProject")
        addNewProjectDialog();
    else if (action->data().toString() == "delProject")
        deleteProject(project);
}

void MainWindow::on_twChapters_customContextMenuRequested(const QPoint &pos)
{
    QMenu menu;
    int volume_id, chapter_id;

    if (!ui->twChapters->currentItem()->parent())
    {
        // selected root item - volume
        volume_id = ui->twChapters->currentItem()->data(0, Qt::UserRole).toInt();
        chapter_id = -1;
        menu.addAction(QObject::tr("&Добавить главу"))->setData("addChapter");
        menu.addAction(QObject::tr("&Добавить том"))->setData("addVolume");
        menu.addAction(QObject::tr("&Редактировать"))->setData("editVolume");
        menu.addSeparator();
        menu.addAction(QObject::tr("&Удалить"))->setData("delVolume");
    }
    else
    {
        // selected children item - chapter
        volume_id = ui->twChapters->currentItem()->parent()->data(0, Qt::UserRole).toInt();
        chapter_id = ui->twChapters->currentItem()->data(0, Qt::UserRole).toInt();

        menu.addAction(QObject::tr("&Выбрать активной"))->setData("setActive");
        menu.addAction(QObject::tr("&Добавить главу"))->setData("addChapterEx");
        menu.addAction(QObject::tr("&Редактировать"))->setData("editVolume");
        menu.addSeparator();
        menu.addAction(QObject::tr("&Удалить"))->setData("delChapter");
    }
    this->setEnabledMenu(menu);

    QVolume *volume = this->scanlate->getActiveProject()->getVolumeById(volume_id);
    QChapter *chapter = volume->getChapterById(chapter_id);

    QAction* action;
    if (pos != QPoint(0,0))
        action =  menu.exec(ui->twChapters->viewport()->mapToGlobal(pos));
    else
        return;

    if(!action)
        return;
    else if (action->data().toString() == "setActive")
        setActiveChapter(chapter);
    else if ((action->data().toString() == "addChapter") || (action->data().toString() == "addChapterEx"))
        addNewChapterDialog(volume);
    else if (action->data().toString() == "addVolume")
        addNewVolumeDialog();
    else if (action->data().toString() == "editVolume")
        volumePropertiesDialog(volume);
    else if (action->data().toString() == "delVolume")
        deleteVolume(volume);
    else if (action->data().toString() == "delChapter")
        deleteChapter(volume, chapter);
}

void MainWindow::saveState()
{
    scanlate->saveState("./data/");
}

void MainWindow::on_MainWindow_destroyed()
{
    scanlate->saveState("./data/");
}
<|endoftext|>


<commit_before>// Copyright (c) 2009 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#define _CRT_SECURE_NO_WARNINGS

#include "base/command_line.h"
#include "base/eintr_wrapper.h"
#include "base/file_path.h"
#include "base/multiprocess_test.h"
#include "base/path_service.h"
#include "base/platform_thread.h"
#include "base/process_util.h"
#include "testing/gtest/include/gtest/gtest.h"

#if defined(OS_LINUX)
#include <dlfcn.h>
#endif
#if defined(OS_POSIX)
#include <fcntl.h>
#include <sys/socket.h>
#endif
#if defined(OS_WIN)
#include <windows.h>
#endif

namespace base {

class ProcessUtilTest : public MultiProcessTest {
#if defined(OS_POSIX)
 public:
  // Spawn a child process that counts how many file descriptors are open.
  int CountOpenFDsInChild();
#endif
};

MULTIPROCESS_TEST_MAIN(SimpleChildProcess) {
  return 0;
}

TEST_F(ProcessUtilTest, SpawnChild) {
  ProcessHandle handle = this->SpawnChild(L"SimpleChildProcess");

  ASSERT_NE(base::kNullProcessHandle, handle);
  EXPECT_TRUE(WaitForSingleProcess(handle, 5000));
  base::CloseProcessHandle(handle);
}

MULTIPROCESS_TEST_MAIN(SlowChildProcess) {
  // Sleep until file "SlowChildProcess.die" is created.
  FILE *fp;
  do {
    PlatformThread::Sleep(100);
    fp = fopen("SlowChildProcess.die", "r");
  } while (!fp);
  fclose(fp);
  remove("SlowChildProcess.die");
  exit(0);
  return 0;
}

TEST_F(ProcessUtilTest, KillSlowChild) {
  remove("SlowChildProcess.die");
  ProcessHandle handle = this->SpawnChild(L"SlowChildProcess");
  ASSERT_NE(base::kNullProcessHandle, handle);
  FILE *fp = fopen("SlowChildProcess.die", "w");
  fclose(fp);
  EXPECT_TRUE(base::WaitForSingleProcess(handle, 5000));
  base::CloseProcessHandle(handle);
}

// TODO(estade): if possible, port these 2 tests.
#if defined(OS_WIN)
TEST_F(ProcessUtilTest, EnableLFH) {
  ASSERT_TRUE(EnableLowFragmentationHeap());
  if (IsDebuggerPresent()) {
    // Under these conditions, LFH can't be enabled. There's no point to test
    // anything.
    const char* no_debug_env = getenv("_NO_DEBUG_HEAP");
    if (!no_debug_env || strcmp(no_debug_env, "1"))
      return;
  }
  HANDLE heaps[1024] = { 0 };
  unsigned number_heaps = GetProcessHeaps(1024, heaps);
  EXPECT_GT(number_heaps, 0u);
  for (unsigned i = 0; i < number_heaps; ++i) {
    ULONG flag = 0;
    SIZE_T length;
    ASSERT_NE(0, HeapQueryInformation(heaps[i],
                                      HeapCompatibilityInformation,
                                      &flag,
                                      sizeof(flag),
                                      &length));
    // If flag is 0, the heap is a standard heap that does not support
    // look-asides. If flag is 1, the heap supports look-asides. If flag is 2,
    // the heap is a low-fragmentation heap (LFH). Note that look-asides are not
    // supported on the LFH.

    // We don't have any documented way of querying the HEAP_NO_SERIALIZE flag.
    EXPECT_LE(flag, 2u);
    EXPECT_NE(flag, 1u);
  }
}

TEST_F(ProcessUtilTest, CalcFreeMemory) {
  ProcessMetrics* metrics =
      ProcessMetrics::CreateProcessMetrics(::GetCurrentProcess());
  ASSERT_TRUE(NULL != metrics);

  // Typical values here is ~1900 for total and ~1000 for largest. Obviously
  // it depends in what other tests have done to this process.
  FreeMBytes free_mem1 = {0};
  EXPECT_TRUE(metrics->CalculateFreeMemory(&free_mem1));
  EXPECT_LT(10u, free_mem1.total);
  EXPECT_LT(10u, free_mem1.largest);
  EXPECT_GT(2048u, free_mem1.total);
  EXPECT_GT(2048u, free_mem1.largest);
  EXPECT_GE(free_mem1.total, free_mem1.largest);
  EXPECT_TRUE(NULL != free_mem1.largest_ptr);

  // Allocate 20M and check again. It should have gone down.
  const int kAllocMB = 20;
  char* alloc = new char[kAllocMB * 1024 * 1024];
  EXPECT_TRUE(NULL != alloc);

  size_t expected_total = free_mem1.total - kAllocMB;
  size_t expected_largest = free_mem1.largest;

  FreeMBytes free_mem2 = {0};
  EXPECT_TRUE(metrics->CalculateFreeMemory(&free_mem2));
  EXPECT_GE(free_mem2.total, free_mem2.largest);
  EXPECT_GE(expected_total, free_mem2.total);
  EXPECT_GE(expected_largest, free_mem2.largest);
  EXPECT_TRUE(NULL != free_mem2.largest_ptr);

  delete[] alloc;
  delete metrics;
}

TEST_F(ProcessUtilTest, GetAppOutput) {
  // Let's create a decently long message.
  std::string message;
  for (int i = 0; i < 1025; i++) {  // 1025 so it does not end on a kilo-byte
                                    // boundary.
    message += "Hello!";
  }

  FilePath python_runtime;
  ASSERT_TRUE(PathService::Get(base::DIR_SOURCE_ROOT, &python_runtime));
  python_runtime = python_runtime.Append(FILE_PATH_LITERAL("third_party"))
                                 .Append(FILE_PATH_LITERAL("python_24"))
                                 .Append(FILE_PATH_LITERAL("python.exe"));

  CommandLine cmd_line(python_runtime.value());
  cmd_line.AppendLooseValue(L"-c");
  cmd_line.AppendLooseValue(L"\"import sys; sys.stdout.write('" +
      ASCIIToWide(message) + L"');\"");
  std::string output;
  ASSERT_TRUE(base::GetAppOutput(cmd_line, &output));
  EXPECT_EQ(message, output);

  // Let's make sure stderr is ignored.
  CommandLine other_cmd_line(python_runtime.value());
  other_cmd_line.AppendLooseValue(L"-c");
  other_cmd_line.AppendLooseValue(
      L"\"import sys; sys.stderr.write('Hello!');\"");
  output.clear();
  ASSERT_TRUE(base::GetAppOutput(other_cmd_line, &output));
  EXPECT_EQ("", output);
}
#endif  // defined(OS_WIN)

#if defined(OS_POSIX)
// Returns the maximum number of files that a process can have open.
// Returns 0 on error.
int GetMaxFilesOpenInProcess() {
  struct rlimit rlim;
  if (getrlimit(RLIMIT_NOFILE, &rlim) != 0) {
    return 0;
  }

  // rlim_t is a uint64 - clip to maxint. We do this since FD #s are ints
  // which are all 32 bits on the supported platforms.
  rlim_t max_int = static_cast<rlim_t>(std::numeric_limits<int32>::max());
  if (rlim.rlim_cur > max_int) {
    return max_int;
  }

  return rlim.rlim_cur;
}

const int kChildPipe = 20;  // FD # for write end of pipe in child process.
MULTIPROCESS_TEST_MAIN(ProcessUtilsLeakFDChildProcess) {
  // This child process counts the number of open FDs, it then writes that
  // number out to a pipe connected to the parent.
  int num_open_files = 0;
  int write_pipe = kChildPipe;
  int max_files = GetMaxFilesOpenInProcess();
  for (int i = STDERR_FILENO + 1; i < max_files; i++) {
    if (i != kChildPipe) {
      if (HANDLE_EINTR(close(i)) != -1) {
        num_open_files += 1;
      }
    }
  }

  int written = HANDLE_EINTR(write(write_pipe, &num_open_files,
                                   sizeof(num_open_files)));
  DCHECK_EQ(static_cast<size_t>(written), sizeof(num_open_files));
  HANDLE_EINTR(close(write_pipe));

  return 0;
}

int ProcessUtilTest::CountOpenFDsInChild() {
  int fds[2];
  if (pipe(fds) < 0)
    NOTREACHED();

  file_handle_mapping_vector fd_mapping_vec;
  fd_mapping_vec.push_back(std::pair<int,int>(fds[1], kChildPipe));
  ProcessHandle handle = this->SpawnChild(L"ProcessUtilsLeakFDChildProcess",
                                          fd_mapping_vec,
                                          false);
  CHECK(handle);
  HANDLE_EINTR(close(fds[1]));

  // Read number of open files in client process from pipe;
  int num_open_files = -1;
  ssize_t bytes_read =
      HANDLE_EINTR(read(fds[0], &num_open_files, sizeof(num_open_files)));
  CHECK(bytes_read == static_cast<ssize_t>(sizeof(num_open_files)));

  CHECK(WaitForSingleProcess(handle, 1000));
  base::CloseProcessHandle(handle);
  HANDLE_EINTR(close(fds[0]));

  return num_open_files;
}

TEST_F(ProcessUtilTest, FDRemapping) {
  int fds_before = CountOpenFDsInChild();

  // open some dummy fds to make sure they don't propogate over to the
  // child process.
  int dev_null = open("/dev/null", O_RDONLY);
  int sockets[2];
  socketpair(AF_UNIX, SOCK_STREAM, 0, sockets);

  int fds_after = CountOpenFDsInChild();

  ASSERT_EQ(fds_after, fds_before);

  HANDLE_EINTR(close(sockets[0]));
  HANDLE_EINTR(close(sockets[1]));
  HANDLE_EINTR(close(dev_null));
}

TEST_F(ProcessUtilTest, GetAppOutput) {
  std::string output;
  EXPECT_TRUE(GetAppOutput(CommandLine(L"true"), &output));
  EXPECT_STREQ("", output.c_str());

  EXPECT_FALSE(GetAppOutput(CommandLine(L"false"), &output));

  std::vector<std::string> argv;
  argv.push_back("/bin/echo");
  argv.push_back("-n");
  argv.push_back("foobar42");
  EXPECT_TRUE(GetAppOutput(CommandLine(argv), &output));
  EXPECT_STREQ("foobar42", output.c_str());
}

#if defined(OS_LINUX)
TEST_F(ProcessUtilTest, GetParentProcessId) {
  base::ProcessId ppid = GetParentProcessId(GetCurrentProcId());
  EXPECT_EQ(ppid, getppid());
}

TEST_F(ProcessUtilTest, ParseProcStatCPU) {
  // /proc/self/stat for a process running "top".
  const char kTopStat[] = "960 (top) S 16230 960 16230 34818 960 "
      "4202496 471 0 0 0 "
      "12 16 0 0 "  // <- These are the goods.
      "20 0 1 0 121946157 15077376 314 18446744073709551615 4194304 "
      "4246868 140733983044336 18446744073709551615 140244213071219 "
      "0 0 0 138047495 0 0 0 17 1 0 0 0 0 0";
  EXPECT_EQ(12 + 16, ParseProcStatCPU(kTopStat));

  // cat /proc/self/stat on a random other machine I have.
  const char kSelfStat[] = "5364 (cat) R 5354 5364 5354 34819 5364 "
      "0 142 0 0 0 "
      "0 0 0 0 "  // <- No CPU, apparently.
      "16 0 1 0 1676099790 2957312 114 4294967295 134512640 134528148 "
      "3221224832 3221224344 3086339742 0 0 0 0 0 0 0 17 0 0 0";

  EXPECT_EQ(0, ParseProcStatCPU(kSelfStat));
}
#endif

#endif  // defined(OS_POSIX)

}  // namespace base
<commit_msg>Fix ProcessUtilTest.FDRemapping hang on x86_64 by counting open fds in child process nondestructively BUG=25270 TEST=base_unittests  --gtest_filter=ProcessUtilTest.FDRemapping  on x86_64 linux<commit_after>// Copyright (c) 2009 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#define _CRT_SECURE_NO_WARNINGS

#include "base/command_line.h"
#include "base/eintr_wrapper.h"
#include "base/file_path.h"
#include "base/multiprocess_test.h"
#include "base/path_service.h"
#include "base/platform_thread.h"
#include "base/process_util.h"
#include "testing/gtest/include/gtest/gtest.h"

#if defined(OS_LINUX)
#include <dlfcn.h>
#endif
#if defined(OS_POSIX)
#include <fcntl.h>
#include <sys/socket.h>
#endif
#if defined(OS_WIN)
#include <windows.h>
#endif

namespace base {

class ProcessUtilTest : public MultiProcessTest {
#if defined(OS_POSIX)
 public:
  // Spawn a child process that counts how many file descriptors are open.
  int CountOpenFDsInChild();
#endif
};

MULTIPROCESS_TEST_MAIN(SimpleChildProcess) {
  return 0;
}

TEST_F(ProcessUtilTest, SpawnChild) {
  ProcessHandle handle = this->SpawnChild(L"SimpleChildProcess");

  ASSERT_NE(base::kNullProcessHandle, handle);
  EXPECT_TRUE(WaitForSingleProcess(handle, 5000));
  base::CloseProcessHandle(handle);
}

MULTIPROCESS_TEST_MAIN(SlowChildProcess) {
  // Sleep until file "SlowChildProcess.die" is created.
  FILE *fp;
  do {
    PlatformThread::Sleep(100);
    fp = fopen("SlowChildProcess.die", "r");
  } while (!fp);
  fclose(fp);
  remove("SlowChildProcess.die");
  exit(0);
  return 0;
}

TEST_F(ProcessUtilTest, KillSlowChild) {
  remove("SlowChildProcess.die");
  ProcessHandle handle = this->SpawnChild(L"SlowChildProcess");
  ASSERT_NE(base::kNullProcessHandle, handle);
  FILE *fp = fopen("SlowChildProcess.die", "w");
  fclose(fp);
  EXPECT_TRUE(base::WaitForSingleProcess(handle, 5000));
  base::CloseProcessHandle(handle);
}

// TODO(estade): if possible, port these 2 tests.
#if defined(OS_WIN)
TEST_F(ProcessUtilTest, EnableLFH) {
  ASSERT_TRUE(EnableLowFragmentationHeap());
  if (IsDebuggerPresent()) {
    // Under these conditions, LFH can't be enabled. There's no point to test
    // anything.
    const char* no_debug_env = getenv("_NO_DEBUG_HEAP");
    if (!no_debug_env || strcmp(no_debug_env, "1"))
      return;
  }
  HANDLE heaps[1024] = { 0 };
  unsigned number_heaps = GetProcessHeaps(1024, heaps);
  EXPECT_GT(number_heaps, 0u);
  for (unsigned i = 0; i < number_heaps; ++i) {
    ULONG flag = 0;
    SIZE_T length;
    ASSERT_NE(0, HeapQueryInformation(heaps[i],
                                      HeapCompatibilityInformation,
                                      &flag,
                                      sizeof(flag),
                                      &length));
    // If flag is 0, the heap is a standard heap that does not support
    // look-asides. If flag is 1, the heap supports look-asides. If flag is 2,
    // the heap is a low-fragmentation heap (LFH). Note that look-asides are not
    // supported on the LFH.

    // We don't have any documented way of querying the HEAP_NO_SERIALIZE flag.
    EXPECT_LE(flag, 2u);
    EXPECT_NE(flag, 1u);
  }
}

TEST_F(ProcessUtilTest, CalcFreeMemory) {
  ProcessMetrics* metrics =
      ProcessMetrics::CreateProcessMetrics(::GetCurrentProcess());
  ASSERT_TRUE(NULL != metrics);

  // Typical values here is ~1900 for total and ~1000 for largest. Obviously
  // it depends in what other tests have done to this process.
  FreeMBytes free_mem1 = {0};
  EXPECT_TRUE(metrics->CalculateFreeMemory(&free_mem1));
  EXPECT_LT(10u, free_mem1.total);
  EXPECT_LT(10u, free_mem1.largest);
  EXPECT_GT(2048u, free_mem1.total);
  EXPECT_GT(2048u, free_mem1.largest);
  EXPECT_GE(free_mem1.total, free_mem1.largest);
  EXPECT_TRUE(NULL != free_mem1.largest_ptr);

  // Allocate 20M and check again. It should have gone down.
  const int kAllocMB = 20;
  char* alloc = new char[kAllocMB * 1024 * 1024];
  EXPECT_TRUE(NULL != alloc);

  size_t expected_total = free_mem1.total - kAllocMB;
  size_t expected_largest = free_mem1.largest;

  FreeMBytes free_mem2 = {0};
  EXPECT_TRUE(metrics->CalculateFreeMemory(&free_mem2));
  EXPECT_GE(free_mem2.total, free_mem2.largest);
  EXPECT_GE(expected_total, free_mem2.total);
  EXPECT_GE(expected_largest, free_mem2.largest);
  EXPECT_TRUE(NULL != free_mem2.largest_ptr);

  delete[] alloc;
  delete metrics;
}

TEST_F(ProcessUtilTest, GetAppOutput) {
  // Let's create a decently long message.
  std::string message;
  for (int i = 0; i < 1025; i++) {  // 1025 so it does not end on a kilo-byte
                                    // boundary.
    message += "Hello!";
  }

  FilePath python_runtime;
  ASSERT_TRUE(PathService::Get(base::DIR_SOURCE_ROOT, &python_runtime));
  python_runtime = python_runtime.Append(FILE_PATH_LITERAL("third_party"))
                                 .Append(FILE_PATH_LITERAL("python_24"))
                                 .Append(FILE_PATH_LITERAL("python.exe"));

  CommandLine cmd_line(python_runtime.value());
  cmd_line.AppendLooseValue(L"-c");
  cmd_line.AppendLooseValue(L"\"import sys; sys.stdout.write('" +
      ASCIIToWide(message) + L"');\"");
  std::string output;
  ASSERT_TRUE(base::GetAppOutput(cmd_line, &output));
  EXPECT_EQ(message, output);

  // Let's make sure stderr is ignored.
  CommandLine other_cmd_line(python_runtime.value());
  other_cmd_line.AppendLooseValue(L"-c");
  other_cmd_line.AppendLooseValue(
      L"\"import sys; sys.stderr.write('Hello!');\"");
  output.clear();
  ASSERT_TRUE(base::GetAppOutput(other_cmd_line, &output));
  EXPECT_EQ("", output);
}
#endif  // defined(OS_WIN)

#if defined(OS_POSIX)
// Returns the maximum number of files that a process can have open.
// Returns 0 on error.
int GetMaxFilesOpenInProcess() {
  struct rlimit rlim;
  if (getrlimit(RLIMIT_NOFILE, &rlim) != 0) {
    return 0;
  }

  // rlim_t is a uint64 - clip to maxint. We do this since FD #s are ints
  // which are all 32 bits on the supported platforms.
  rlim_t max_int = static_cast<rlim_t>(std::numeric_limits<int32>::max());
  if (rlim.rlim_cur > max_int) {
    return max_int;
  }

  return rlim.rlim_cur;
}

const int kChildPipe = 20;  // FD # for write end of pipe in child process.
MULTIPROCESS_TEST_MAIN(ProcessUtilsLeakFDChildProcess) {
  // This child process counts the number of open FDs, it then writes that
  // number out to a pipe connected to the parent.
  int num_open_files = 0;
  int write_pipe = kChildPipe;
  int max_files = GetMaxFilesOpenInProcess();
  for (int i = STDERR_FILENO + 1; i < max_files; i++) {
    if (i != kChildPipe) {
      int fd;
      if ((fd = HANDLE_EINTR(dup(i))) != -1) {
        close(fd);
        num_open_files += 1;
      }
    }
  }

  int written = HANDLE_EINTR(write(write_pipe, &num_open_files,
                                   sizeof(num_open_files)));
  DCHECK_EQ(static_cast<size_t>(written), sizeof(num_open_files));
  HANDLE_EINTR(close(write_pipe));

  return 0;
}

int ProcessUtilTest::CountOpenFDsInChild() {
  int fds[2];
  if (pipe(fds) < 0)
    NOTREACHED();

  file_handle_mapping_vector fd_mapping_vec;
  fd_mapping_vec.push_back(std::pair<int,int>(fds[1], kChildPipe));
  ProcessHandle handle = this->SpawnChild(L"ProcessUtilsLeakFDChildProcess",
                                          fd_mapping_vec,
                                          false);
  CHECK(handle);
  HANDLE_EINTR(close(fds[1]));

  // Read number of open files in client process from pipe;
  int num_open_files = -1;
  ssize_t bytes_read =
      HANDLE_EINTR(read(fds[0], &num_open_files, sizeof(num_open_files)));
  CHECK(bytes_read == static_cast<ssize_t>(sizeof(num_open_files)));

  CHECK(WaitForSingleProcess(handle, 1000));
  base::CloseProcessHandle(handle);
  HANDLE_EINTR(close(fds[0]));

  return num_open_files;
}

TEST_F(ProcessUtilTest, FDRemapping) {
  int fds_before = CountOpenFDsInChild();

  // open some dummy fds to make sure they don't propogate over to the
  // child process.
  int dev_null = open("/dev/null", O_RDONLY);
  int sockets[2];
  socketpair(AF_UNIX, SOCK_STREAM, 0, sockets);

  int fds_after = CountOpenFDsInChild();

  ASSERT_EQ(fds_after, fds_before);

  HANDLE_EINTR(close(sockets[0]));
  HANDLE_EINTR(close(sockets[1]));
  HANDLE_EINTR(close(dev_null));
}

TEST_F(ProcessUtilTest, GetAppOutput) {
  std::string output;
  EXPECT_TRUE(GetAppOutput(CommandLine(L"true"), &output));
  EXPECT_STREQ("", output.c_str());

  EXPECT_FALSE(GetAppOutput(CommandLine(L"false"), &output));

  std::vector<std::string> argv;
  argv.push_back("/bin/echo");
  argv.push_back("-n");
  argv.push_back("foobar42");
  EXPECT_TRUE(GetAppOutput(CommandLine(argv), &output));
  EXPECT_STREQ("foobar42", output.c_str());
}

#if defined(OS_LINUX)
TEST_F(ProcessUtilTest, GetParentProcessId) {
  base::ProcessId ppid = GetParentProcessId(GetCurrentProcId());
  EXPECT_EQ(ppid, getppid());
}

TEST_F(ProcessUtilTest, ParseProcStatCPU) {
  // /proc/self/stat for a process running "top".
  const char kTopStat[] = "960 (top) S 16230 960 16230 34818 960 "
      "4202496 471 0 0 0 "
      "12 16 0 0 "  // <- These are the goods.
      "20 0 1 0 121946157 15077376 314 18446744073709551615 4194304 "
      "4246868 140733983044336 18446744073709551615 140244213071219 "
      "0 0 0 138047495 0 0 0 17 1 0 0 0 0 0";
  EXPECT_EQ(12 + 16, ParseProcStatCPU(kTopStat));

  // cat /proc/self/stat on a random other machine I have.
  const char kSelfStat[] = "5364 (cat) R 5354 5364 5354 34819 5364 "
      "0 142 0 0 0 "
      "0 0 0 0 "  // <- No CPU, apparently.
      "16 0 1 0 1676099790 2957312 114 4294967295 134512640 134528148 "
      "3221224832 3221224344 3086339742 0 0 0 0 0 0 0 17 0 0 0";

  EXPECT_EQ(0, ParseProcStatCPU(kSelfStat));
}
#endif

#endif  // defined(OS_POSIX)

}  // namespace base
<|endoftext|>


<commit_before>/*
 * Copyright (c) 2016-2017 dresden elektronik ingenieurtechnik gmbh.
 * All rights reserved.
 *
 * The software in this package is published under the terms of the BSD
 * style license a copy of which has been included with this distribution in
 * the LICENSE.txt file.
 *
 */

#include <QString>
#include <QTcpSocket>
#include <QTimer>
#include <QFile>
#include <QUdpSocket>
#include <QVariantMap>
#include "de_web_plugin.h"
#include "de_web_plugin_private.h"
#include "gateway_scanner.h"

/*! Inits the UPnP discorvery. */
void DeRestPluginPrivate::initUpnpDiscovery()
{
    DBG_Assert(udpSock == 0);

    initDescriptionXml();

    if (deCONZ::appArgumentNumeric("--upnp", 1) == 0)
    {
        udpSock = 0;
        udpSockOut = 0;
        joinedMulticastGroup = false;
        return;
    }

    udpSock = new QUdpSocket(this);
    udpSockOut = new QUdpSocket(this);
    joinedMulticastGroup = false;

    connect(udpSock, SIGNAL(readyRead()),
            this, SLOT(upnpReadyRead()));

    upnpTimer = new QTimer(this);
    upnpTimer->setSingleShot(false);
    connect(upnpTimer, SIGNAL(timeout()),
            this, SLOT(announceUpnp()));
    upnpTimer->start(1000); // setup phase fast interval
}

/*! Replaces description_in.xml template with dynamic content. */
void DeRestPluginPrivate::initDescriptionXml()
{
    deCONZ::ApsController *apsCtrl = deCONZ::ApsController::instance();
    QString serverRoot = apsCtrl->getParameter(deCONZ::ParamHttpRoot);

    if (!serverRoot.isEmpty())
    {
        descriptionXml.clear();
        QFile f(serverRoot + "/description_in.xml");

        if (f.open(QFile::ReadOnly))
        {
            QByteArray line;
            do {
               line = f.readLine(320);
               if (!line.isEmpty())
               {
                   line.replace(QString("{{PORT}}"), qPrintable(QString::number(gwPort)));
                   line.replace(QString("{{IPADDRESS}}"), qPrintable(gwIpAddress));
                   line.replace(QString("{{UUID}}"), qPrintable(gwUuid));
                   line.replace(QString("{{GWNAME}}"), qPrintable(gwName));
                   line.replace(QString("{{BRIDGEID}}"), qPrintable(gwBridgeId));
                   descriptionXml.append(line);

                   DBG_Printf(DBG_INFO_L2, "%s", line.constData());
               }
             } while (!line.isEmpty());
        }
    }
}

/*! Sends SSDP broadcast for announcement. */
void DeRestPluginPrivate::announceUpnp()
{
    if (udpSock->state() != QAbstractSocket::BoundState)
    {
        joinedMulticastGroup = false;
        DBG_Printf(DBG_ERROR, "UPNP socket not bound, state: %d\n", udpSock->state());
        // retry
        if (!udpSock->bind(QHostAddress("0.0.0.0"), 1900, QUdpSocket::ShareAddress | QUdpSocket::ReuseAddressHint)) // SSDP
        {
            DBG_Printf(DBG_ERROR, "UPNP error %s\n", qPrintable(udpSock->errorString()));
        }
        return;
    }

    if (!joinedMulticastGroup)
    {
        QHostAddress groupAddress("239.255.255.250");
        if (!udpSock->joinMulticastGroup(groupAddress))
        {
            DBG_Printf(DBG_ERROR, "UPNP error %s\n", qPrintable(udpSock->errorString()));
            return;
        }
        joinedMulticastGroup = true;
    }

    if (upnpTimer->interval() != (20 * 1000))
        upnpTimer->start(20 * 1000);

    quint16 port = 1900;
    QHostAddress host;
    QByteArray datagram = QString(QLatin1String(
    "NOTIFY * HTTP/1.1\r\n"
    "HOST: 239.255.255.250:1900\r\n"
    "CACHE-CONTROL: max-age=100\r\n"
    "LOCATION: http://%1:%2/description.xml\r\n"
    "SERVER: FreeRTOS/7.4.2, UPnP/1.0, IpBridge/1.8.0\r\n"
    "NTS: ssdp:alive\r\n"
    "NT: upnp:rootdevice\r\n"
    "USN: uuid:%3::upnp:rootdevice\r\n"
    "GWID.phoscon.de: %4\r\n"
    "hue-bridgeid: %4\r\n"
    "\r\n"))
            .arg(gwConfig["ipaddress"].toString())
            .arg(gwConfig["port"].toDouble())
            .arg(gwConfig["uuid"].toString())
            .arg(gwBridgeId).toLocal8Bit();

    host.setAddress(QLatin1String("239.255.255.250"));

    if (udpSockOut->writeDatagram(datagram, host, port) == -1)
    {
        DBG_Printf(DBG_ERROR, "UDP send error %s\n", qPrintable(udpSockOut->errorString()));
    }
}

/*! Handles SSDP packets. */
void DeRestPluginPrivate::upnpReadyRead()
{
    while (udpSock->hasPendingDatagrams())
    {
        quint16 port;
        QHostAddress host;
        QByteArray datagram;
        datagram.resize(udpSock->pendingDatagramSize());
        udpSock->readDatagram(datagram.data(), datagram.size(), &host, &port);

        QTextStream stream(datagram);
        QString searchTarget;
        QString location;

        if (DBG_IsEnabled(DBG_HTTP))
        {
            DBG_Printf(DBG_HTTP, "%s\n", qPrintable(datagram));
        }

        while (!stream.atEnd())
        {
            QString line = stream.readLine();

            if (!searchTarget.isEmpty())
            {
                break;
            }

            if (line.startsWith(QLatin1String("LOCATION:")))
            {
                location = line;
            }
            else if (line.startsWith(QLatin1String("GWID.phoscon.de")))
            {
                searchTarget = line;
            }
            else if (line.startsWith(QLatin1String("hue-bridgeid")))
            {
                searchTarget = line;
            }
            else if (line.startsWith(QLatin1String("ST:")))
            {
                if (line.contains(QLatin1String("ssdp:all")) ||
                    line.contains(QLatin1String("device:basic")) ||
                    line.contains(QLatin1String("upnp:rootdevice")))
                {
                    searchTarget = line;
                }
            }
        }

        if (searchTarget.isEmpty())
        {}
        else if (datagram.startsWith("M-SEARCH *"))
        {

            DBG_Printf(DBG_HTTP, "UPNP %s:%u\n%s\n", qPrintable(host.toString()), port, datagram.data());
            datagram.clear();

            datagram.append("HTTP/1.1 200 OK\r\n");
            datagram.append("CACHE-CONTROL: max-age=100\r\n");
            datagram.append("EXT:\r\n");
            datagram.append(QString("LOCATION: http://%1:%2/description.xml\r\n")
                            .arg(gwConfig["ipaddress"].toString())
                            .arg(gwConfig["port"].toDouble()).toLocal8Bit());
            datagram.append("SERVER: FreeRTOS/7.4.2, UPnP/1.0, IpBridge/1.8.0\r\n");
            datagram.append(searchTarget);
            datagram.append(QLatin1String("\r\n"));
            datagram.append(QString("USN: uuid:%1::upnp:rootdevice\r\n").arg(gwUuid));
            datagram.append(QString("GWID.phoscon.de: %1\r\n").arg(gwBridgeId));
            datagram.append(QString("hue-bridgeid: %1\r\n").arg(gwBridgeId));
            datagram.append("\r\n");

            if (udpSockOut->writeDatagram(datagram.data(), datagram.size(), host, port) == -1)
            {
                DBG_Printf(DBG_ERROR, "UDP send error %s\n", qPrintable(udpSockOut->errorString()));
            }
        }
        else if (datagram.startsWith("NOTIFY *") && !location.isEmpty())
        {
            // phoscon gateway or hue bridge
            QStringList ls = searchTarget.split(' ');
            if (ls.size() != 2)
            {
                continue;
            }

            if (ls[1] == gwBridgeId)
            {
                continue; // self
            }

            ls = location.split(' ');
            if (ls.size() != 2 || !ls[1].startsWith(QLatin1String("http://")))
            {
                continue;
            }

            // http://192.168.14.103:80/description.xml
            location = ls[1];
            //location.remove(QLatin1String("http://"));
            int idx = location.indexOf('/');
            if (idx == -1)
            {
                continue;
            }
            location = location.left(idx);
            location += QLatin1String("/api/config");

            gwScanner->queryGateway(location);
        }
    }
}
<commit_msg>Use QUrl to dissect UPnP location<commit_after>/*
 * Copyright (c) 2016-2017 dresden elektronik ingenieurtechnik gmbh.
 * All rights reserved.
 *
 * The software in this package is published under the terms of the BSD
 * style license a copy of which has been included with this distribution in
 * the LICENSE.txt file.
 *
 */

#include <QString>
#include <QTcpSocket>
#include <QTimer>
#include <QFile>
#include <QUdpSocket>
#include <QVariantMap>
#include "de_web_plugin.h"
#include "de_web_plugin_private.h"
#include "gateway_scanner.h"

/*! Inits the UPnP discorvery. */
void DeRestPluginPrivate::initUpnpDiscovery()
{
    DBG_Assert(udpSock == 0);

    initDescriptionXml();

    if (deCONZ::appArgumentNumeric("--upnp", 1) == 0)
    {
        udpSock = 0;
        udpSockOut = 0;
        joinedMulticastGroup = false;
        return;
    }

    udpSock = new QUdpSocket(this);
    udpSockOut = new QUdpSocket(this);
    joinedMulticastGroup = false;

    connect(udpSock, SIGNAL(readyRead()),
            this, SLOT(upnpReadyRead()));

    upnpTimer = new QTimer(this);
    upnpTimer->setSingleShot(false);
    connect(upnpTimer, SIGNAL(timeout()),
            this, SLOT(announceUpnp()));
    upnpTimer->start(1000); // setup phase fast interval
}

/*! Replaces description_in.xml template with dynamic content. */
void DeRestPluginPrivate::initDescriptionXml()
{
    deCONZ::ApsController *apsCtrl = deCONZ::ApsController::instance();
    QString serverRoot = apsCtrl->getParameter(deCONZ::ParamHttpRoot);

    if (!serverRoot.isEmpty())
    {
        descriptionXml.clear();
        QFile f(serverRoot + "/description_in.xml");

        if (f.open(QFile::ReadOnly))
        {
            QByteArray line;
            do {
               line = f.readLine(320);
               if (!line.isEmpty())
               {
                   line.replace(QString("{{PORT}}"), qPrintable(QString::number(gwPort)));
                   line.replace(QString("{{IPADDRESS}}"), qPrintable(gwIpAddress));
                   line.replace(QString("{{UUID}}"), qPrintable(gwUuid));
                   line.replace(QString("{{GWNAME}}"), qPrintable(gwName));
                   line.replace(QString("{{BRIDGEID}}"), qPrintable(gwBridgeId));
                   descriptionXml.append(line);

                   DBG_Printf(DBG_INFO_L2, "%s", line.constData());
               }
             } while (!line.isEmpty());
        }
    }
}

/*! Sends SSDP broadcast for announcement. */
void DeRestPluginPrivate::announceUpnp()
{
    if (udpSock->state() != QAbstractSocket::BoundState)
    {
        joinedMulticastGroup = false;
        DBG_Printf(DBG_ERROR, "UPNP socket not bound, state: %d\n", udpSock->state());
        // retry
        if (!udpSock->bind(QHostAddress("0.0.0.0"), 1900, QUdpSocket::ShareAddress | QUdpSocket::ReuseAddressHint)) // SSDP
        {
            DBG_Printf(DBG_ERROR, "UPNP error %s\n", qPrintable(udpSock->errorString()));
        }
        return;
    }

    if (!joinedMulticastGroup)
    {
        QHostAddress groupAddress("239.255.255.250");
        if (!udpSock->joinMulticastGroup(groupAddress))
        {
            DBG_Printf(DBG_ERROR, "UPNP error %s\n", qPrintable(udpSock->errorString()));
            return;
        }
        joinedMulticastGroup = true;
    }

    if (upnpTimer->interval() != (20 * 1000))
        upnpTimer->start(20 * 1000);

    quint16 port = 1900;
    QHostAddress host;
    QByteArray datagram = QString(QLatin1String(
    "NOTIFY * HTTP/1.1\r\n"
    "HOST: 239.255.255.250:1900\r\n"
    "CACHE-CONTROL: max-age=100\r\n"
    "LOCATION: http://%1:%2/description.xml\r\n"
    "SERVER: FreeRTOS/7.4.2, UPnP/1.0, IpBridge/1.8.0\r\n"
    "NTS: ssdp:alive\r\n"
    "NT: upnp:rootdevice\r\n"
    "USN: uuid:%3::upnp:rootdevice\r\n"
    "GWID.phoscon.de: %4\r\n"
    "hue-bridgeid: %4\r\n"
    "\r\n"))
            .arg(gwConfig["ipaddress"].toString())
            .arg(gwConfig["port"].toDouble())
            .arg(gwConfig["uuid"].toString())
            .arg(gwBridgeId).toLocal8Bit();

    host.setAddress(QLatin1String("239.255.255.250"));

    if (udpSockOut->writeDatagram(datagram, host, port) == -1)
    {
        DBG_Printf(DBG_ERROR, "UDP send error %s\n", qPrintable(udpSockOut->errorString()));
    }
}

/*! Handles SSDP packets. */
void DeRestPluginPrivate::upnpReadyRead()
{
    while (udpSock->hasPendingDatagrams())
    {
        quint16 port;
        QHostAddress host;
        QByteArray datagram;
        datagram.resize(udpSock->pendingDatagramSize());
        udpSock->readDatagram(datagram.data(), datagram.size(), &host, &port);

        QTextStream stream(datagram);
        QString searchTarget;
        QString location;

        if (DBG_IsEnabled(DBG_HTTP))
        {
            DBG_Printf(DBG_HTTP, "%s\n", qPrintable(datagram));
        }

        while (!stream.atEnd())
        {
            QString line = stream.readLine();

            if (!searchTarget.isEmpty())
            {
                break;
            }

            if (line.startsWith(QLatin1String("LOCATION:")))
            {
                location = line;
            }
            else if (line.startsWith(QLatin1String("GWID.phoscon.de")))
            {
                searchTarget = line;
            }
            else if (line.startsWith(QLatin1String("hue-bridgeid")))
            {
                searchTarget = line;
            }
            else if (line.startsWith(QLatin1String("ST:")))
            {
                if (line.contains(QLatin1String("ssdp:all")) ||
                    line.contains(QLatin1String("device:basic")) ||
                    line.contains(QLatin1String("upnp:rootdevice")))
                {
                    searchTarget = line;
                }
            }
        }

        if (searchTarget.isEmpty())
        {}
        else if (datagram.startsWith("M-SEARCH *"))
        {

            DBG_Printf(DBG_HTTP, "UPNP %s:%u\n%s\n", qPrintable(host.toString()), port, datagram.data());
            datagram.clear();

            datagram.append("HTTP/1.1 200 OK\r\n");
            datagram.append("CACHE-CONTROL: max-age=100\r\n");
            datagram.append("EXT:\r\n");
            datagram.append(QString("LOCATION: http://%1:%2/description.xml\r\n")
                            .arg(gwConfig["ipaddress"].toString())
                            .arg(gwConfig["port"].toDouble()).toLocal8Bit());
            datagram.append("SERVER: FreeRTOS/7.4.2, UPnP/1.0, IpBridge/1.8.0\r\n");
            datagram.append(searchTarget);
            datagram.append(QLatin1String("\r\n"));
            datagram.append(QString("USN: uuid:%1::upnp:rootdevice\r\n").arg(gwUuid));
            datagram.append(QString("GWID.phoscon.de: %1\r\n").arg(gwBridgeId));
            datagram.append(QString("hue-bridgeid: %1\r\n").arg(gwBridgeId));
            datagram.append("\r\n");

            if (udpSockOut->writeDatagram(datagram.data(), datagram.size(), host, port) == -1)
            {
                DBG_Printf(DBG_ERROR, "UDP send error %s\n", qPrintable(udpSockOut->errorString()));
            }
        }
        else if (datagram.startsWith("NOTIFY *") && !location.isEmpty())
        {
            // phoscon gateway or hue bridge
            QStringList ls = searchTarget.split(' ');
            if (ls.size() != 2)
            {
                continue;
            }

            if (ls[1] == gwBridgeId)
            {
                continue; // self
            }

            ls = location.split(' ');
            if (ls.size() != 2 || !ls[1].startsWith(QLatin1String("http://")))
            {
                continue;
            }

            // http://192.168.14.103:80/description.xml

            QUrl url(ls[1]);
            location = QString("http://%1:%2/api/config").arg(url.host()).arg(url.port(80));
            gwScanner->queryGateway(location);
        }
    }
}
<|endoftext|>