Skip to content
API Documentation
BastionGuard™
TeeJee.Process.vala
Go to the documentation of this file.
1
2/*
3 * TeeJee.ProcessHelper.vala
4 *
5 * Copyright 2012-2018 Tony George <teejeetech@gmail.com>
6 *
7 * This program is free software; you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation; either version 2 of the License, or
10 * (at your option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License
18 * along with this program; if not, write to the Free Software
19 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
20 * MA 02110-1301, USA.
21 *
22 *
23 */
24
26
27 using TeeJee.Logging;
29 using TeeJee.Misc;
30
31 public string TEMP_DIR;
32
33 /* Convenience functions for executing commands and managing processes */
34
35 // execute process ---------------------------------
36
37 public static void init_tmp(){
38
39 // a list of folders where temp files could be stored
40 string[] tempPlaces = {
41 Environment.get_tmp_dir(), // system temp dir
42 "/var/tmp", // another system temp dir, if the first one failed, this one is likely to fail too
43 Environment.get_home_dir() + "/.temp", // user temp dir
44 "/dev/shm", // shared memory
45 };
46
47 foreach (string tempPlace in tempPlaces) {
48 string std_out, std_err;
49
50 TEMP_DIR = tempPlace + "/bastionguard-backup-" + random_string();
52 Posix.chmod(TEMP_DIR, 0750);
53 exec_script_sync("echo 'ok'",out std_out,out std_err, true);
54
55 if ((std_out == null) || (std_out.strip() != "ok")){
56 // this dir does not work for some reason - probably no disk space
58 } else {
59 // script worked - we have found a tempdir to use
60 return;
61 }
62 }
63
64 stderr.printf("No usable temp directory was found!\n");
65 }
66
67 public int exec_sync (string cmd, out string? std_out = null, out string? std_err = null){
68
69 /* Executes single command synchronously.
70 * Pipes and multiple commands are not supported.
71 * std_out, std_err can be null. Output will be written to terminal if null. */
72
73 try {
74 int status;
75 Process.spawn_command_line_sync(cmd, out std_out, out std_err, out status);
76 return status;
77 }
78 catch (Error e){
79 log_error (e.message);
80 return -1;
81 }
82 }
83
84 public int exec_script_sync (string script,
85 out string? std_out = null, out string? std_err = null,
86 bool supress_errors = false, bool run_as_admin = false,
87 bool cleanup_tmp = true, bool print_to_terminal = false){
88
89 /* Executes commands synchronously.
90 * Pipes and multiple commands are fully supported.
91 * Commands are written to a temporary bash script and executed.
92 * std_out, std_err can be null. Output will be written to terminal if null.
93 * */
94
95 string? sh_file = save_bash_script_temp(script, null, true, supress_errors);
96 if (sh_file == null) {
97 // saving the script failed
98 return -1;
99 }
100
101 string sh_file_admin = "";
102
103 if (run_as_admin){
104
105 var script_admin = "#!/usr/bin/env bash\n";
106 script_admin += "pkexec env DISPLAY=$DISPLAY XAUTHORITY=$XAUTHORITY";
107 script_admin += " '%s'".printf(escape_single_quote(sh_file));
108
109 sh_file_admin = GLib.Path.build_filename(file_parent(sh_file),"script-admin.sh");
110
111 save_bash_script_temp(script_admin, sh_file_admin, true, supress_errors);
112 }
113
114 try {
115 string[] argv = new string[1];
116 if (run_as_admin){
117 argv[0] = sh_file_admin;
118 }
119 else{
120 argv[0] = sh_file;
121 }
122
123 string[] env = Environ.get();
124
125 int exit_code;
126
127 if (print_to_terminal){
128
129 Process.spawn_sync (
130 TEMP_DIR, //working dir
131 argv, //argv
132 env, //environment
133 SpawnFlags.SEARCH_PATH,
134 null, // child_setup
135 null,
136 null,
137 out exit_code
138 );
139 }
140 else{
141
142 Process.spawn_sync (
143 TEMP_DIR, //working dir
144 argv, //argv
145 env, //environment
146 SpawnFlags.SEARCH_PATH,
147 null, // child_setup
148 out std_out,
149 out std_err,
150 out exit_code
151 );
152 }
153
154 if (cleanup_tmp){
155 file_delete(sh_file);
156 if (run_as_admin){
157 file_delete(sh_file_admin);
158 }
159 }
160
161 return exit_code;
162 }
163 catch (Error e){
164 if (!supress_errors){
165 log_error (e.message);
166 }
167 return -1;
168 }
169 }
170
171 public int exec_script_async (string script){
172
173 /* Executes commands synchronously.
174 * Pipes and multiple commands are fully supported.
175 * Commands are written to a temporary bash script and executed.
176 * Return value indicates if script was started successfully.
177 * */
178
179 try {
180
181 string scriptfile = save_bash_script_temp (script);
182
183 string[] argv = new string[1];
184 argv[0] = scriptfile;
185
186 string[] env = Environ.get();
187
188 Pid child_pid;
189 Process.spawn_async_with_pipes(
190 TEMP_DIR, //working dir
191 argv, //argv
192 env, //environment
193 SpawnFlags.SEARCH_PATH,
194 null,
195 out child_pid);
196
197 return 0;
198 }
199 catch (Error e){
200 log_error (e.message);
201 return 1;
202 }
203 }
204
205 /**
206 executes a command as the "normal" unprivileged user async
207 may execute the command as root if the user could not be determined or the name could not be resolved
208 */
209 public static int exec_user_async(string command) {
210 // find correct user
211 int uid = TeeJee.System.get_user_id();
212 string cmd = command;
213 if(uid > 0) {
214 // non root
215 string? user = TeeJee.System.get_username_from_uid(uid);
216 if(user != null) {
217 cmd = "pkexec --user %s env DISPLAY=$DISPLAY XAUTHORITY=$XAUTHORITY DBUS_SESSION_BUS_ADDRESS=$DBUS_SESSION_BUS_ADDRESS ".printf(user) + cmd;
218 }
219 }
220
221 log_debug(cmd);
222 return TeeJee.ProcessHelper.exec_script_async(cmd);
223 }
224
225 public string? save_bash_script_temp (string commands, string? script_path = null,
226 bool force_locale = true, bool supress_errors = false){
227
228 string sh_path = script_path;
229
230 /* Creates a temporary bash script with given commands
231 * Returns the script file path */
232
233 var script = new StringBuilder();
234 script.append ("#!/usr/bin/env bash\n");
235 script.append ("\n");
236 if (force_locale){
237 script.append("LANG=C\n");
238 script.append("LC_ALL=C.UTF-8\n");
239 }
240 script.append ("\n");
241 script.append ("%s\n".printf(commands));
242 script.append ("\n\nexitCode=$?\n");
243 script.append ("echo ${exitCode} > status\n");
244
245 if ((sh_path == null) || (sh_path.length == 0)){
246 sh_path = get_temp_file_path();
247 }
248
249 try{
250 //write script file
251 var file = File.new_for_path (sh_path);
252 if (file.query_exists ()) {
253 file.delete ();
254 }
255 var file_stream = file.create (FileCreateFlags.REPLACE_DESTINATION);
256 var data_stream = new DataOutputStream (file_stream);
257 data_stream.put_string (script.str);
258 data_stream.close();
259
260 // set execute permission
261 Posix.chmod (sh_path, 0744);
262
263 return sh_path;
264 }
265 catch (Error e) {
266 if (!supress_errors){
267 log_error (e.message);
268 }
269 }
270
271 return null;
272 }
273
274 public string get_temp_file_path(){
275
276 /* Generates temporary file path */
277
278 return TEMP_DIR + "/" + timestamp_numeric() + (new Rand()).next_int().to_string();
279 }
280
281 // find process -------------------------------
282
283 public static bool cmd_exists(string cmd_tool){
284 string? path = Environment.find_program_in_path(cmd_tool);
285 return (path != null) && (path.length > 0);
286 }
287
288 // return the name of the executable of a given pid or self if pid is <= 0
289 // returns an empty string on error or if the pid could not be found
290 public string get_process_exe_name(long pid = -1){
291 string pidStr = (pid <= 0 ? "self" : pid.to_string());
292 string path = "/proc/%s/exe".printf(pidStr);
293 string link;
294 try {
295 link = GLib.FileUtils.read_link(path);
296 } catch (Error e) {
297 return "";
298 }
299
300 return GLib.Path.get_basename(link);
301 }
302
303 // get the parent pid of process or self
304 public Pid get_process_parent(Pid process = -1) {
305 string pidStr = (process <= 0 ? "self" : process.to_string());
306 string path = "/proc/%s/stat".printf(pidStr);
307 string stats = file_read(path);
308 string details = stats.split(")", 2)[1];
309 string[] splitted = details.split(" ", 3);
310 if(splitted.length == 3) {
311 return int.parse(splitted[2]);
312 }
313
314 log_debug("can not parse process stat %s".printf(stats));
315 return -1;
316 }
317
318 // get the effective user pid of an process
319 // returns -1 on error
320 public int get_euid_of_process(Pid process) {
321 GLib.File file = GLib.File.new_for_path("/proc/%d".printf(process));
322 try {
323 GLib.FileInfo info = file.query_info(FileAttribute.UNIX_UID, GLib.FileQueryInfoFlags.NONE);
324 return (int) info.get_attribute_uint32(FileAttribute.UNIX_UID);
325 } catch(GLib.Error e) {
326 log_debug("failed to fetch user of process %i %s".printf(process, e.message));
327 }
328 return -1;
329 }
330
331 // find the first parent process, that is owned by the user and not root
332 public Pid get_user_process() {
333 Pid ppid = -1;
334 int targetUser = TeeJee.System.get_user_id();
335 int user = 0;
336
337 do {
338 ppid = get_process_parent(ppid);
339 user = get_euid_of_process (ppid);
340 } while(user != targetUser && ppid > 1);
341 if(user == targetUser) {
342 return ppid;
343 }
344 return -1;
345 }
346
347 // get the env of an process
348 public string[]? get_process_env(Pid pid) {
349 if(pid < 1) {
350 return null;
351 }
352 return file_read_array("/proc/%i/environ".printf(pid), '\0');
353 }
354
355 // get the value of name in env if it exists or return default_value
356 public string? get_env(string[] env, string name, string? default_value = null) {
357 foreach(string env_var in env) {
358 string[] splitted = env_var.split("=", 2);
359 if(splitted[0] == name) {
360 if (splitted.length == 2) {
361 return splitted[1];
362 }
363 }
364 }
365 return default_value;
366 }
367
368 public Pid[] get_process_children (Pid parent_pid){
369
370 /* Returns the list of child processes owned by a given process */
371
372 // no explicit check for the existence of /proc/ as this might be a time-of-check-time-of-use bug.
373 File procfs = File.new_for_path("/proc/");
374
375 try {
376 FileEnumerator enumerator = procfs.enumerate_children(FileAttribute.STANDARD_NAME, FileQueryInfoFlags.NOFOLLOW_SYMLINKS);
377 FileInfo info;
378 Pid[] childList = {};
379 while ((info = enumerator.next_file()) != null) {
380 if(info.get_file_type() != FileType.DIRECTORY) {
381 // only interested in directories
382 continue;
383 }
384
385 string name = info.get_name();
386
387 uint64 pid;
388 if(!uint64.try_parse(name, out pid)) {
389 // make sure to not access any other directories that may be present in /proc for some reason
390 continue;
391 }
392
393 string? fileCont = file_read("/proc/%s/stat".printf(name));
394 if(fileCont == null) {
395 // stat file of pid might not be readable (because of permissions or the process died since we got its pid)
396 continue;
397 }
398
399 // the format of the stat file is documented in man 5 proc
400 // it begging is: pid (comm) status ppid ...
401
402 // the process name could contain a space or ) and confuse the parsing.
403 // so we make sure to take the last ) and only parse the stuff after that.
404 int index = fileCont.last_index_of_char(')');
405 string parseline = fileCont.substring(index);
406 string[] split = parseline.split(" ", 4); // we are not interested in the part after ppid so just leave it a big string
407 if(split.length != 4) {
408 // format of stat file is not matching - should never happen
409 log_error("can not parse state of %ld".printf((long) pid));
410 continue;
411 }
412
413 uint64 ppid = uint64.parse(split[2]);
414 if(ppid != 0 && ppid == parent_pid) {
415 // the process is a child of the target parent process
416 childList += (Pid) pid;
417 }
418 }
419 return childList;
420 } catch (Error e) {
421 log_error(e.message);
422 log_error("Failed to get child processes of %ld".printf(parent_pid));
423 }
424 return {};
425 }
426
427 // manage process ---------------------------------
428
429 public void process_quit(Pid process_pid, bool killChildren = true){
430
431 /* Terminates specified process and its children (optional).
432 * Sends signal SIGTERM to the process to allow it to quit gracefully.
433 * */
434
435 process_send_signal(process_pid, Posix.Signal.TERM, killChildren);
436 }
437
438 public void process_kill(Pid process_pid, bool killChildren = true) {
439
440 /* Kills specified process and its children (optional).
441 * Sends signal SIGKILL to the process to kill it forcefully.
442 * It is recommended to use the function process_quit() instead.
443 * */
444
445 process_send_signal(process_pid, Posix.Signal.KILL, killChildren);
446 }
447
448 public void process_send_signal(Pid process_pid, Posix.Signal sig, bool children = true) {
449
450 /* Sends a signal to a process and its children (optional). */
451
452 // get the childs before sending the signal, as the childs might not be accessible afterwards
453 Pid[] child_pids = get_process_children (process_pid);
454 Posix.kill (process_pid, sig);
455
456 if (children){
457 foreach (Pid pid in child_pids){
458 Posix.kill (pid, sig);
459 }
460 }
461 }
462
463 // process priority ---------------------------------------
464
465 public void process_set_priority (Pid procID, int prio){
466
467 /* Set process priority */
468
469 if (Posix.getpriority (Posix.PRIO_PROCESS, procID) != prio)
470 Posix.setpriority (Posix.PRIO_PROCESS, procID, prio);
471 }
472
473 public int process_get_priority (Pid procID){
474
475 /* Get process priority */
476
477 return Posix.getpriority (Posix.PRIO_PROCESS, procID);
478 }
479
480 public void process_set_priority_normal (Pid procID){
481
482 /* Set normal priority for process */
483
484 process_set_priority (procID, 0);
485 }
486
487 public void process_set_priority_low (Pid procID){
488
489 /* Set low priority for process */
490
491 process_set_priority (procID, 5);
492 }
493}
string escape_single_quote(string file_path)
string? file_read(string file_path)
string file_parent(string file_path)
string?[] file_read_array(string file_path, char delimiter='\n')
bool file_delete(string file_path)
static bool dir_delete(string dir_path, bool show_message=false)
bool dir_create(string dir_path, bool show_message=false)
void log_debug(string message)
void log_error(string message, bool highlight=false, bool is_warning=false)
string timestamp_numeric()
string random_string(int length=8, string charset="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890")
Pid get_process_parent(Pid process=-1)
int exec_script_async(string script)
int get_euid_of_process(Pid process)
int exec_script_sync(string script, out string? std_out=null, out string? std_err=null, bool supress_errors=false, bool run_as_admin=false, bool cleanup_tmp=true, bool print_to_terminal=false)
void process_send_signal(Pid process_pid, Posix.Signal sig, bool children=true)
void process_set_priority_low(Pid procID)
string?[] get_process_env(Pid pid)
void process_set_priority_normal(Pid procID)
void process_quit(Pid process_pid, bool killChildren=true)
string? get_env(string[] env, string name, string? default_value=null)
int process_get_priority(Pid procID)
void process_set_priority(Pid procID, int prio)
int exec_sync(string cmd, out string? std_out=null, out string? std_err=null)
void process_kill(Pid process_pid, bool killChildren=true)
static int exec_user_async(string command)
string? save_bash_script_temp(string commands, string? script_path=null, bool force_locale=true, bool supress_errors=false)
string get_process_exe_name(long pid=-1)
static bool cmd_exists(string cmd_tool)
Pid[] get_process_children(Pid parent_pid)