Skip to content

Commit 7f506b4

Browse files
authored
Implement /GUPDATE for remote updates - Requested by @siniStar7
This module adds the /GUPDATE command to trigger updates.sh across all network servers, allowing server operators to initiate updates remotely.
1 parent ed941c5 commit 7f506b4

1 file changed

Lines changed: 251 additions & 0 deletions

File tree

src/modules/m_remoteupdate.cpp

Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
/*
2+
* InspIRCd -- Internet Relay Chat Daemon
3+
*
4+
* Copyright (C) 2026 IRC4Fun
5+
*
6+
* This file is part of InspIRCd. InspIRCd is free software: you can
7+
* redistribute it and/or modify it under the terms of the GNU General Public
8+
* License as published by the Free Software Foundation, version 2.
9+
*
10+
* This program is distributed in the hope that it will be useful, but WITHOUT
11+
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
12+
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
13+
* details.
14+
*
15+
* You should have received a copy of the GNU General Public License
16+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
*/
18+
19+
/// $ModAuthor: IRC4Fun
20+
/// $ModDesc: Adds the /GUPDATE command to trigger updates.sh across all network servers.
21+
22+
#include "inspircd.h"
23+
24+
#ifndef _WIN32
25+
# include <fcntl.h>
26+
# include <sys/wait.h>
27+
# include <unistd.h>
28+
#endif
29+
30+
class CommandGUpdate;
31+
32+
class UpdateCheckTimer final
33+
: public Timer
34+
{
35+
private:
36+
pid_t childpid;
37+
std::string requestor;
38+
std::string logfile;
39+
CommandGUpdate& cmd;
40+
41+
public:
42+
UpdateCheckTimer(pid_t pid, const std::string& nick, const std::string& log, CommandGUpdate& c)
43+
: Timer(2, true)
44+
, childpid(pid)
45+
, requestor(nick)
46+
, logfile(log)
47+
, cmd(c)
48+
{
49+
}
50+
51+
bool Tick() override;
52+
};
53+
54+
class CommandGUpdate final
55+
: public Command
56+
{
57+
friend class UpdateCheckTimer;
58+
59+
private:
60+
std::string scriptpath;
61+
std::string logpath;
62+
bool updating = false;
63+
UpdateCheckTimer* activetimer = nullptr;
64+
pid_t childpid = -1;
65+
66+
public:
67+
CommandGUpdate(Module* Creator)
68+
: Command(Creator, "GUPDATE", 0, 1)
69+
{
70+
access_needed = CmdAccess::OPERATOR;
71+
syntax = { "[<servermask>]" };
72+
}
73+
74+
~CommandGUpdate()
75+
{
76+
// Clean up timer if module is unloaded mid-update.
77+
// Timer destructor calls DelTimer, safely removing from TimerManager.
78+
delete activetimer;
79+
80+
// Reap child process if still running to prevent zombies.
81+
if (childpid > 0)
82+
{
83+
int status;
84+
if (waitpid(childpid, &status, WNOHANG) == 0)
85+
{
86+
// Child still running; send SIGTERM and do a brief wait.
87+
kill(childpid, SIGTERM);
88+
waitpid(childpid, &status, 0);
89+
}
90+
}
91+
}
92+
93+
void SetPaths(const std::string& script, const std::string& log)
94+
{
95+
scriptpath = script;
96+
logpath = log;
97+
}
98+
99+
CmdResult Handle(User* user, const Params& parameters) override
100+
{
101+
std::string servermask = parameters.empty() ? "*" : parameters[0];
102+
103+
if (!InspIRCd::Match(ServerInstance->Config->ServerName, servermask))
104+
{
105+
ServerInstance->SNO.WriteToSnoMask('a', "UPDATE broadcast by '{}' (not targeting this server).", user->nick);
106+
return CmdResult::SUCCESS;
107+
}
108+
109+
if (updating)
110+
{
111+
user->WriteNotice(INSP_FORMAT("*** UPDATE already in progress on {}.", ServerInstance->Config->ServerName));
112+
return CmdResult::FAILURE;
113+
}
114+
115+
if (access(scriptpath.c_str(), X_OK) != 0)
116+
{
117+
user->WriteNotice(INSP_FORMAT("*** UPDATE script not found or not executable: {}", scriptpath));
118+
ServerInstance->SNO.WriteToSnoMask('a', "UPDATE on \x02{}\x02 \x03" "04FAILED\x03: script '{}' not found or not executable.",
119+
ServerInstance->Config->ServerName, scriptpath);
120+
return CmdResult::FAILURE;
121+
}
122+
123+
std::string timestamp = std::to_string(ServerInstance->Time());
124+
std::string logfile = logpath + "/update-" + timestamp + ".log";
125+
126+
ServerInstance->SNO.WriteToSnoMask('a', "UPDATE on \x02{}\x02 \x03" "08started\x03 by '{}'. Running: {}",
127+
ServerInstance->Config->ServerName, user->nick, scriptpath);
128+
129+
pid_t pid = fork();
130+
131+
if (pid == -1)
132+
{
133+
user->WriteNotice(INSP_FORMAT("*** UPDATE fork failed on {}: {}", ServerInstance->Config->ServerName, strerror(errno)));
134+
ServerInstance->SNO.WriteToSnoMask('a', "UPDATE on \x02{}\x02 \x03" "04FAILED\x03: fork error: {}",
135+
ServerInstance->Config->ServerName, strerror(errno));
136+
return CmdResult::FAILURE;
137+
}
138+
139+
if (pid == 0)
140+
{
141+
// Child process: close all inherited IRCd file descriptors.
142+
long maxfd = sysconf(_SC_OPEN_MAX);
143+
if (maxfd < 0)
144+
maxfd = 65536;
145+
for (long fd = 3; fd < maxfd; fd++)
146+
close(static_cast<int>(fd));
147+
148+
// Redirect stdout/stderr to log file.
149+
int logfd = open(logfile.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0640);
150+
if (logfd >= 0)
151+
{
152+
dup2(logfd, STDOUT_FILENO);
153+
dup2(logfd, STDERR_FILENO);
154+
if (logfd > STDERR_FILENO)
155+
close(logfd);
156+
}
157+
158+
// Close stdin.
159+
close(STDIN_FILENO);
160+
161+
// Reset signal handlers to defaults.
162+
signal(SIGPIPE, SIG_DFL);
163+
signal(SIGCHLD, SIG_DFL);
164+
165+
// Execute the update script.
166+
execl("/bin/sh", "sh", scriptpath.c_str(), nullptr);
167+
_exit(127);
168+
}
169+
170+
// Parent: track state and poll for completion.
171+
childpid = pid;
172+
updating = true;
173+
activetimer = new UpdateCheckTimer(pid, user->nick, logfile, *this);
174+
ServerInstance->Timers.AddTimer(activetimer);
175+
176+
user->WriteNotice(INSP_FORMAT("*** UPDATE started on {}. You will be notified via snomask 'a' when complete.", ServerInstance->Config->ServerName));
177+
return CmdResult::SUCCESS;
178+
}
179+
180+
RouteDescriptor GetRouting(User* user, const Params& parameters) override
181+
{
182+
return ROUTE_BROADCAST;
183+
}
184+
};
185+
186+
bool UpdateCheckTimer::Tick()
187+
{
188+
int status;
189+
pid_t result = waitpid(childpid, &status, WNOHANG);
190+
191+
if (result == 0)
192+
return true; // Still running, keep polling.
193+
194+
if (result == childpid)
195+
{
196+
if (WIFEXITED(status) && WEXITSTATUS(status) == 0)
197+
{
198+
ServerInstance->SNO.WriteToSnoMask('a', "UPDATE on \x02{}\x02 completed \x03" "03successfully\x03. Modules may need reloading (/GRELOADMODULE). Requested by '{}'.",
199+
ServerInstance->Config->ServerName, requestor);
200+
}
201+
else
202+
{
203+
int exitcode = WIFEXITED(status) ? WEXITSTATUS(status) : -1;
204+
ServerInstance->SNO.WriteToSnoMask('a', "UPDATE on \x02{}\x02 \x03" "04FAILED\x03 (exit code {}). Check log: {}. Requested by '{}'.",
205+
ServerInstance->Config->ServerName, exitcode, logfile, requestor);
206+
}
207+
}
208+
else
209+
{
210+
ServerInstance->SNO.WriteToSnoMask('a', "UPDATE on \x02{}\x02 \x03" "04FAILED\x03 (waitpid error: {}). Requested by '{}'.",
211+
ServerInstance->Config->ServerName, strerror(errno), requestor);
212+
}
213+
214+
// Clean up: clear command state, then self-destruct.
215+
cmd.updating = false;
216+
cmd.childpid = -1;
217+
cmd.activetimer = nullptr;
218+
delete this;
219+
return false;
220+
}
221+
222+
class ModuleRemoteUpdate final
223+
: public Module
224+
{
225+
private:
226+
CommandGUpdate cmd;
227+
228+
public:
229+
ModuleRemoteUpdate()
230+
: Module(VF_OPTCOMMON, "Adds the /GUPDATE command which allows server operators to trigger updates.sh on all network servers.")
231+
, cmd(this)
232+
{
233+
}
234+
235+
void ReadConfig(ConfigStatus& status) override
236+
{
237+
auto tag = ServerInstance->Config->ConfValue("remoteupdate");
238+
std::string script = tag->getString("script", "/home/ircd/updates.sh", 1);
239+
std::string logdir = tag->getString("logdir", "/home/ircd/inspircd4/run/logs", 1);
240+
241+
if (access(script.c_str(), F_OK) != 0)
242+
throw ModuleException(this, "Update script not found: " + script);
243+
244+
if (access(script.c_str(), X_OK) != 0)
245+
throw ModuleException(this, "Update script is not executable (permission denied): " + script);
246+
247+
cmd.SetPaths(script, logdir);
248+
}
249+
};
250+
251+
MODULE_INIT(ModuleRemoteUpdate)

0 commit comments

Comments
 (0)