cedc2a4c49d2c285d5560e82f4014b38b5613d04
[citadel] / appimage / ctdlvisor.c
1 //
2 // This is a supervisor program that handles start/stop/restart of
3 // the various Citadel System components, when we are running on
4 // an AppImage instance.
5 //
6 // Copyright (c) 2021 by the citadel.org team
7 //
8 // This program is open source software.  It runs great on the
9 // Linux operating system (and probably elsewhere).  You can use,
10 // copy, and run it under the terms of the GNU General Public
11 // License version 3.  Richard Stallman is an asshole communist.
12 //
13 // This program is distributed in the hope that it will be useful,
14 // but WITHOUT ANY WARRANTY; without even the implied warranty of
15 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 // GNU General Public License for more details.
17
18
19 #include <stdlib.h>
20 #include <unistd.h>
21 #include <stdio.h>
22 #include <sys/wait.h>
23 #include <errno.h>
24 #include <signal.h>
25 #include <string.h>
26 #include <limits.h>
27 #include <sys/types.h>
28 #include <sys/stat.h>
29
30 char *data_directory = "/usr/local/citadel";
31 char *http_port = "80";
32 char *https_port = "443";
33 pid_t citserver_pid;
34 pid_t webcit_pid;
35 pid_t webcits_pid;
36 int shutting_down = 0;
37
38 // Call this instead of exit() just for common diagnostics etc.
39 void ctdlvisor_exit(int code) {
40         printf("ctdlvisor: exit code %d\n", code);
41         exit(code);
42 }
43
44
45 // Interrupting this program with a signal will begin an orderly shutdown.
46 void signal_handler(int signal) {
47         fprintf(stderr, "ctdlvisor: caught signal %d\n", signal);
48
49         while(shutting_down) {
50                 fprintf(stderr, "ctdlvisor: already shutting down\n");
51                 sleep(1);
52         }
53
54         int status;
55         pid_t who_exited;
56         char *what_exited = NULL;
57
58         shutting_down = 1;
59         kill(citserver_pid, SIGTERM);
60         kill(webcit_pid, SIGTERM);
61         kill(webcits_pid, SIGTERM);
62         do {
63                 fprintf(stderr, "ctdlvisor: waiting for all child process to exit...\n");
64                 who_exited = waitpid(-1, &status, 0);
65                 if (who_exited == citserver_pid) {
66                         what_exited = "Citadel Server";
67                 }
68                 else if (who_exited == webcit_pid) {
69                         what_exited = "WebCit HTTP";
70                 }
71                 else if (who_exited == webcits_pid) {
72                         what_exited = "WebCit HTTPS";
73                 }
74                 else {
75                         what_exited = "unknown";
76                 }
77                 fprintf(stderr, "ctdlvisor: pid=%d (%s) exited, status=%d, exitcode=%d\n", who_exited, what_exited, status, WEXITSTATUS(status));
78         } while (who_exited >= 0);
79
80         ctdlvisor_exit(0);
81 }
82
83
84 void detach_from_tty(void) {
85         signal(SIGHUP, SIG_IGN);
86         signal(SIGINT, SIG_IGN);
87         signal(SIGQUIT, SIG_IGN);
88
89         setsid();       // become our own process group leader
90         umask(0);
91         if (    (freopen("/dev/null", "r", stdin) != stdin) ||
92                 (freopen("/dev/null", "w", stdout) != stdout) ||
93                 (freopen("/dev/null", "w", stderr) != stderr)
94         ) {
95                 fprintf(stderr, "sysdep: unable to reopen stdio: %s\n", strerror(errno));
96         }
97 }
98
99
100 pid_t start_citadel() {
101         char bin[1024];
102         sprintf(bin, "%s/usr/local/citadel/citserver", getenv("APPDIR"));
103         pid_t pid = fork();
104         if (pid == 0) {
105                 fprintf(stderr, "ctdlvisor: executing %s\n", bin);
106                 detach_from_tty();
107                 execlp(bin, "citserver", "-x9", "-h", data_directory, NULL);
108                 exit(errno);
109         }
110         else {
111                 return(pid);
112         }
113 }
114
115
116 pid_t start_webcit() {
117         char bin[1024];
118         sprintf(bin, "%s/usr/local/webcit/webcit", getenv("APPDIR"));
119         char wchome[1024];
120         sprintf(wchome, "-h%s/usr/local/webcit", getenv("APPDIR"));
121         pid_t pid = fork();
122         if (pid == 0) {
123                 fprintf(stderr, "ctdlvisor: executing %s\n", bin);
124                 detach_from_tty();
125                 execlp(bin, "webcit", "-x9", wchome, "-p", http_port, "uds", data_directory, NULL);
126                 exit(errno);
127         }
128         else {
129                 return(pid);
130         }
131 }
132
133
134 pid_t start_webcits() {
135         char bin[1024];
136         sprintf(bin, "%s/usr/local/webcit/webcit", getenv("APPDIR"));
137         char wchome[1024];
138         sprintf(wchome, "-h%s/usr/local/webcit", getenv("APPDIR"));
139         pid_t pid = fork();
140         if (pid == 0) {
141                 fprintf(stderr, "ctdlvisor: executing %s\n", bin);
142                 detach_from_tty();
143                 execlp(bin, "webcit", "-x9", wchome, "-s", "-p", https_port, "uds", data_directory, NULL);
144                 exit(errno);
145         }
146         else {
147                 return(pid);
148         }
149 }
150
151
152 void main_loop(void) {
153         int status;
154         pid_t who_exited;
155         int citserver_exit_code = 0;
156
157         do {
158                 fprintf(stderr, "ctdlvisor: waiting for any child process to exit...\n");
159                 who_exited = waitpid(-1, &status, 0);
160                 fprintf(stderr, "ctdlvisor: pid=%d exited, status=%d, exitcode=%d\n", who_exited, status, WEXITSTATUS(status));
161
162                 // A *deliberate* exit of citserver will cause ctdlvisor to shut the whole AppImage down.
163                 // If it crashes, however, we will start it back up.
164                 if (who_exited == citserver_pid) {
165                         citserver_exit_code = WEXITSTATUS(status);
166                         if (citserver_exit_code == 0) {
167                                 fprintf(stderr, "ctdlvisor: citserver exited normally - ending AppImage session\n");
168                                 shutting_down = 1;
169                                 kill(webcit_pid, SIGTERM);
170                                 kill(webcits_pid, SIGTERM);
171                         }
172                         else if ((citserver_exit_code >= 101) && (citserver_exit_code <= 109)) {
173                                 fprintf(stderr, "ctdlvisor: citserver exited intentionally - ending AppImage session\n");
174                                 shutting_down = 1;
175                                 kill(webcit_pid, SIGTERM);
176                                 kill(webcits_pid, SIGTERM);
177                         }
178                         else {
179                                 citserver_pid = start_citadel();
180                         }
181                 }
182
183                 // WebCit processes are restarted if they exit for any reason.
184                 if ((who_exited == webcit_pid) && (!shutting_down))     webcit_pid = start_webcit();
185                 if ((who_exited == webcits_pid) && (!shutting_down))    webcits_pid = start_webcits();
186
187                 // If we somehow end up in an endless loop, at least slow it down.
188                 sleep(1);
189
190         } while (who_exited >= 0);
191         ctdlvisor_exit(citserver_exit_code);
192 }
193
194
195 void run_in_foreground(void) {
196         fprintf(stderr, "ctdlvisor: Welcome to the Citadel System, brought to you using AppImage.\n");
197         fprintf(stderr, "ctdlvisor: LD_LIBRARY_PATH = %s\n", getenv("LD_LIBRARY_PATH"));
198         fprintf(stderr, "ctdlvisor:            PATH = %s\n", getenv("PATH"));
199         fprintf(stderr, "ctdlvisor:          APPDIR = %s\n", getenv("APPDIR"));
200         fprintf(stderr, "ctdlvisor:  data directory = %s\n", data_directory);
201         fprintf(stderr, "ctdlvisor:       HTTP port = %s\n", http_port);
202         fprintf(stderr, "ctdlvisor:      HTTPS port = %s\n", https_port);
203
204         if (access(data_directory, R_OK|W_OK|X_OK)) {
205                 fprintf(stderr, "ctdlvisor: %s: %s\n", data_directory, strerror(errno));
206                 ctdlvisor_exit(errno);
207         }
208
209         signal(SIGTERM, signal_handler);
210         signal(SIGHUP, signal_handler);
211         signal(SIGINT, signal_handler);
212         signal(SIGQUIT, signal_handler);
213
214         citserver_pid = start_citadel();
215         webcit_pid = start_webcit();
216         webcits_pid = start_webcits();
217
218         main_loop();
219         ctdlvisor_exit(0);
220 }
221
222
223 void install_as_service(void) {
224
225         // FIXME fail if some other citadel distribution is already there
226         // FIXME fail if any server processes are running
227         // FIXME interact with the user
228         // FIXME get port numbers and data directory
229         // FIXME create the data directory
230         // FIXME move the appimage into its permanent location
231
232         fprintf(stderr, "Installing as service\n");
233
234         FILE *fp = fopen("/etc/systemd/system/ctdl.service", "w");
235         fprintf(fp,     "# This unit file starts all Citadel services via the AppImage distribution.\n"
236                         "[Unit]\n"
237                         "Description=Citadel\n"
238                         "After=network.target\n"
239                         "[Service]\n"
240                         "ExecStart=/root/citadel/appimage/Citadel-x86_64.AppImage run -h %s -s %s -s %s\n"
241                         "ExecStop=/bin/kill $MAINPID\n"
242                         "KillMode=process\n"
243                         "Restart=on-failure\n"
244                         "LimitCORE=infinity\n"
245                         "[Install]\n"
246                         "WantedBy=multi-user.target\n"
247                 ,
248                         data_directory, http_port, https_port
249         );
250         fclose(fp);
251
252         fprintf(stderr, "systemd unit file is installed.  Type 'systemctl enable ctdl' to have it start at boot.\n");
253 }
254
255
256 static char *usage =
257         "\n"
258         "ctdlvisor: usage: ctdlvisor [-h data_directory] [-p http_port] [-s https_port] command\n"
259         "           'command' must be one of: run, install, remove, upgrade, test, help\n"
260         "\n"
261 ;
262
263 int main(int argc, char **argv) {
264         int c;
265
266         if (getenv("APPDIR") == NULL) {
267                 fprintf(stderr, "ctdlvisor: APPDIR is not set.  This program must be run from within an AppImage.\n");
268                 ctdlvisor_exit(1);
269         }
270
271         while ((c = getopt (argc, argv, "h:p:s:")) != -1)  switch(c) {
272                 case 'h':
273                         data_directory = optarg;
274                         break;
275                 case 'p':
276                         http_port = optarg;
277                         break;
278                 case 's':
279                         https_port = optarg;
280                         break;
281                 default:
282                         fprintf(stderr, "%s", usage);
283                         ctdlvisor_exit(1);
284         }
285
286
287         if (argc != optind+1) {
288                 fprintf(stderr, "%s", usage);
289                 ctdlvisor_exit(1);
290         }
291
292         if (!strcasecmp(argv[optind], "run")) {
293                 run_in_foreground();
294         }
295         else if (!strcasecmp(argv[optind], "install")) {
296                 install_as_service();
297         }
298         else if (!strcasecmp(argv[optind], "remove")) {
299                 fprintf(stderr, "oops, this is not implemented yet\n");
300         }
301         else if (!strcasecmp(argv[optind], "upgrade")) {
302                 fprintf(stderr, "oops, this is not implemented yet\n");
303         }
304         else if (!strcasecmp(argv[optind], "test")) {
305                 fprintf(stderr, "oops, this is not implemented yet\n");
306         }
307         else if (!strcasecmp(argv[optind], "help")) {
308                 fprintf(stderr, "%s", usage);
309                 fprintf(stderr, "[-h dir]  Use 'dir' as the Citadel data directory (this directory must exist)\n"
310                                 "[-p port] Listen for HTTP connections on 'port'\n"
311                                 "[-s port] Listen for HTTPS connections on 'port'\n"
312                                 "'command' must be one of:\n"
313                                 "       run     - launch Citadel services (does not detach from terminal)\n"
314                                 "       install - create systemd unit files for automatic startup at boot\n"
315                                 "       remove  - delete systemd unit files to end automatic startup\n"
316                                 "       upgrade - download and install a new version of this appimage\n"
317                                 "       test    - test the appimage for binary compatibility with this host\n"
318                                 "       help    - display this message\n"
319                                 "\n"
320                 );
321         }
322         else {
323                 fprintf(stderr, "%s", usage);
324                 ctdlvisor_exit(1);
325         }
326
327         ctdlvisor_exit(0);
328 }