1 /*
2  * Copyright (c) 2010 Mike Massonnet, <mmassonnet@xfce.org>
3  * Copyright (c) 2018 Rozhuk Ivan <rozhuk.im@gmail.com>
4  *
5  * This program is free software; you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation; either version 2 of the License, or (at
8  * your option) any later version.
9  */
10 
11 #ifdef HAVE_CONFIG_H
12 #include <config.h>
13 #endif
14 
15 #include <stdlib.h>
16 #include <unistd.h>
17 #include <sys/types.h>
18 #include <pwd.h>
19 #include <signal.h>
20 #include <sys/resource.h>
21 
22 #include <glib-object.h>
23 #include <glib/gi18n.h>
24 #include <gtk/gtk.h>
25 #include <gmodule.h>
26 
27 #include "task-manager.h"
28 #ifdef HAVE_WNCK
29 #include "app-manager.h"
30 #endif
31 #include "process-tree-view.h" /* for the columns of the model */
32 #include "settings.h"
33 
34 #define TIMESTAMP_DELTA 4
35 
36 
37 
38 static XtmSettings *settings = NULL;
39 static gboolean model_update_forced = FALSE;
40 static gboolean more_precision;
41 static gboolean full_cmdline;
42 
43 
44 
45 typedef struct _XtmTaskManagerClass XtmTaskManagerClass;
46 struct _XtmTaskManagerClass
47 {
48 	GObjectClass		parent_class;
49 };
50 struct _XtmTaskManager
51 {
52 	GObject			parent;
53 	/*<private>*/
54 #ifdef HAVE_WNCK
55 	XtmAppManager *		app_manager;
56 #endif
57 	GtkTreeModel *		model;
58 	GArray *		tasks;
59 	gushort			cpu_count;
60 	gfloat			cpu_user;
61 	gfloat			cpu_system;
62 	guint64			memory_total;
63 	guint64			memory_available; /* free + cache + buffers + an-OS-specific-value */
64 	guint64			memory_free;
65 	guint64			memory_cache;
66 	guint64			memory_buffers;
67 	guint64			swap_total;
68 	guint64			swap_free;
69 };
70 G_DEFINE_TYPE (XtmTaskManager, xtm_task_manager, G_TYPE_OBJECT)
71 
72 static void	xtm_task_manager_finalize			(GObject *object);
73 
74 static void	setting_changed					(GObject *object, GParamSpec *pspec, XtmTaskManager *manager);
75 static void	model_add_task					(XtmTaskManager *manager, Task *task, glong timestamp);
76 static void	model_update_tree_iter				(XtmTaskManager *manager, GtkTreeIter *iter, glong timestamp, gboolean update_cmd_line, Task *task);
77 static void	model_mark_tree_iter_as_removed			(GtkTreeModel *model, GtkTreeIter *iter, glong timestamp);
78 static void	model_remove_tree_iter				(GtkTreeModel *model, GtkTreeIter *iter);
79 static gboolean	task_list_find_for_pid				(GArray *task_list, GPid pid, Task **task, guint *idx);
80 static glong	__current_timestamp				(void);
81 
82 
83 
84 static void
xtm_task_manager_class_init(XtmTaskManagerClass * klass)85 xtm_task_manager_class_init (XtmTaskManagerClass *klass)
86 {
87 	GObjectClass *class = G_OBJECT_CLASS (klass);
88 	xtm_task_manager_parent_class = g_type_class_peek_parent (klass);
89 	class->finalize = xtm_task_manager_finalize;
90 }
91 
92 static void
xtm_task_manager_init(XtmTaskManager * manager)93 xtm_task_manager_init (XtmTaskManager *manager)
94 {
95 #ifdef HAVE_WNCK
96 	manager->app_manager = xtm_app_manager_new ();
97 #endif
98 	manager->tasks = g_array_new (FALSE, FALSE, sizeof (Task));
99 
100 	/* Listen to settings changes and force an update on the whole model */
101 	settings = xtm_settings_get_default ();
102 	g_object_get (settings, "more-precision", &more_precision, NULL);
103 	g_object_get (settings, "full-command-line", &full_cmdline, NULL);
104 	g_signal_connect (settings, "notify::more-precision", G_CALLBACK (setting_changed), manager);
105 	g_signal_connect (settings, "notify::full-command-line", G_CALLBACK (setting_changed), manager);
106 }
107 
108 static void
xtm_task_manager_finalize(GObject * object)109 xtm_task_manager_finalize (GObject *object)
110 {
111 	XtmTaskManager *manager = XTM_TASK_MANAGER (object);
112 	g_array_free (manager->tasks, TRUE);
113 #ifdef HAVE_WNCK
114 	g_object_unref (manager->app_manager);
115 #endif
116 	g_object_unref (settings);
117 }
118 
119 static void
setting_changed(GObject * object,GParamSpec * pspec __unused,XtmTaskManager * manager __unused)120 setting_changed (GObject *object, GParamSpec *pspec __unused, XtmTaskManager *manager __unused)
121 {
122 	g_object_get (object, "more-precision", &more_precision, NULL);
123 	g_object_get (object, "full-command-line", &full_cmdline, NULL);
124 	model_update_forced = TRUE;
125 }
126 
127 static gchar *
pretty_cmdline(gchar * cmdline,gchar * comm)128 pretty_cmdline (gchar *cmdline, gchar *comm)
129 {
130 	gunichar c;
131 	gchar *ch, *text_max, *text = g_strstrip (g_strdup (cmdline));
132 	gsize csize, text_size = (gsize)strlen (text);
133 
134 	/* UTF-8 normalize. */
135 	do {
136 		for (ch = text, text_max = (text + text_size);
137 		     text_max > ch;
138 		     text_max = (text + text_size), ch = g_utf8_next_char(ch)) {
139 			c = g_utf8_get_char_validated(ch, -1); /* If use (text_max - ch) - result is worse. */
140 			if ((gunichar)-2 == c) {
141 				text_size = (gsize)(ch - text);
142 				(*ch) = 0;
143 				break;
144 			}
145 			if ((gunichar)-1 == c) {
146 				(*ch) = ' ';
147 				continue;
148 			}
149 			csize = (gsize)g_unichar_to_utf8(c, NULL);
150 
151 			if (!g_unichar_isdefined(c) ||
152 			    !g_unichar_isprint(c) ||
153 			    (g_unichar_isspace(c) && (1 != csize || (' ' != (*ch) && '	' != (*ch)))) ||
154 			    g_unichar_ismark(c) ||
155 			    g_unichar_istitle(c) ||
156 			    g_unichar_iswide(c) ||
157 			    g_unichar_iszerowidth(c) ||
158 			    g_unichar_iscntrl(c)) {
159 				if (text_max < (ch + csize))
160 					break;
161 				memmove(ch, (ch + csize), (gsize)(text_max - (ch + csize)));
162 				text_size -= csize;
163 			}
164 		}
165 		text[text_size] = 0;
166 	} while (!g_utf8_validate(text, (gssize)text_size, NULL));
167 
168 	if (!full_cmdline && text_size > 3)
169 	{
170 		/* Shorten full path to commands and wine applications */
171 		if (text[0] == '/' || (g_ascii_isupper (text[0]) && text[1] == ':' && text[2] == '\\'))
172 		{
173 			gchar *p = g_strstr_len (text, (gssize)text_size, comm);
174 			if (p != NULL)
175 			{
176 				memmove (text, p, (gsize)(text_size - (gsize)(p - text) + 1));
177 			}
178 		}
179 	}
180 	return text;
181 }
182 
183 static void
_xtm_task_manager_set_model(XtmTaskManager * manager,GtkTreeModel * model)184 _xtm_task_manager_set_model (XtmTaskManager *manager, GtkTreeModel *model)
185 {
186 	g_return_if_fail (XTM_IS_TASK_MANAGER (manager));
187 	g_return_if_fail (GTK_IS_TREE_MODEL (model));
188 	manager->model = model;
189 }
190 
191 static void
model_add_task(XtmTaskManager * manager,Task * task,glong timestamp)192 model_add_task (XtmTaskManager *manager, Task *task, glong timestamp)
193 {
194 	GtkTreeIter iter;
195 	GtkTreeModel *model = manager->model;
196 	gchar *uid_name = get_uid_name (task->uid);
197 
198 	gtk_list_store_append (GTK_LIST_STORE (model), &iter);
199 	gtk_list_store_set (GTK_LIST_STORE (model), &iter,
200 		XTM_PTV_COLUMN_PID, task->pid,
201 		XTM_PTV_COLUMN_STATE, task->state,
202 		XTM_PTV_COLUMN_UID, task->uid,
203 		XTM_PTV_COLUMN_UID_STR, uid_name,
204 		XTM_PTV_COLUMN_TIMESTAMP, timestamp,
205 		-1);
206 	g_free(uid_name);
207 	model_update_tree_iter (manager, &iter, timestamp, TRUE, task);
208 }
209 
210 static void
model_mark_tree_iter_as_removed(GtkTreeModel * model,GtkTreeIter * iter,glong timestamp)211 model_mark_tree_iter_as_removed (GtkTreeModel *model, GtkTreeIter *iter, glong timestamp)
212 {
213 	gtk_list_store_set (GTK_LIST_STORE (model), iter,
214 		XTM_PTV_COLUMN_CPU, 0.0,
215 		XTM_PTV_COLUMN_CPU_STR, "-",
216 		XTM_PTV_COLUMN_BACKGROUND, "#e57373",
217 		XTM_PTV_COLUMN_FOREGROUND, "#000000",
218 		XTM_PTV_COLUMN_TIMESTAMP, timestamp,
219 		-1);
220 }
221 
222 static void
model_remove_tree_iter(GtkTreeModel * model,GtkTreeIter * iter)223 model_remove_tree_iter (GtkTreeModel *model, GtkTreeIter *iter)
224 {
225 	gtk_list_store_remove (GTK_LIST_STORE (model), iter);
226 }
227 
228 static void
model_update_tree_iter(XtmTaskManager * manager,GtkTreeIter * iter,glong timestamp,gboolean update_cmd_line,Task * task)229 model_update_tree_iter (XtmTaskManager *manager, GtkTreeIter *iter, glong timestamp, gboolean update_cmd_line, Task *task)
230 {
231 	GtkTreeModel *model = manager->model;
232 	gchar *vsz, *rss, cpu[16];
233 	gchar value[14];
234 	glong old_timestamp;
235 	gchar *old_state;
236 	gchar *background, *foreground;
237 #ifdef HAVE_WNCK
238 	App *app = xtm_app_manager_get_app_from_pid (manager->app_manager, task->pid);
239 	GdkPixbuf *icon;
240 #endif
241 
242 	vsz = g_format_size_full (task->vsz, G_FORMAT_SIZE_IEC_UNITS);
243 	rss = g_format_size_full (task->rss, G_FORMAT_SIZE_IEC_UNITS);
244 
245 	g_snprintf (value, sizeof(value), (more_precision) ? "%.2f" : "%.0f", (task->cpu_user + task->cpu_system));
246 	g_snprintf (cpu, sizeof(cpu), _("%s%%"), value);
247 
248 	/* Retrieve values for tweaking background/foreground color and updating content as needed */
249 	gtk_tree_model_get (model, iter, XTM_PTV_COLUMN_TIMESTAMP, &old_timestamp, XTM_PTV_COLUMN_STATE, &old_state,
250 			XTM_PTV_COLUMN_BACKGROUND, &background, XTM_PTV_COLUMN_FOREGROUND, &foreground,
251 #ifdef HAVE_WNCK
252 			XTM_PTV_COLUMN_ICON, &icon,
253 #endif
254 			-1);
255 
256 #ifdef HAVE_WNCK
257 	if (app != NULL && icon == NULL)
258 		gtk_list_store_set (GTK_LIST_STORE (model), iter, XTM_PTV_COLUMN_ICON, app->icon, -1);
259 
260 	if (app != NULL && full_cmdline == FALSE)
261 	{
262 		gchar *cmdline = g_strdup (app->name);
263 		gtk_list_store_set (GTK_LIST_STORE (model), iter, XTM_PTV_COLUMN_COMMAND, cmdline, -1);
264 		g_free (cmdline);
265 	}
266 	else if (update_cmd_line)
267 #else
268 	if (update_cmd_line)
269 #endif
270 	{
271 		gchar *cmdline = pretty_cmdline (task->cmdline, task->name);
272 		gtk_list_store_set (GTK_LIST_STORE (model), iter, XTM_PTV_COLUMN_COMMAND, cmdline, -1);
273 		g_free (cmdline);
274 	}
275 
276 	if (g_strcmp0 (task->state, old_state) != 0 && background == NULL)
277 	{
278 		/* Set yellow color for changing state */
279 		background = g_strdup ("#fff176");
280 		foreground = g_strdup ("#000000");
281 		old_timestamp = timestamp - TIMESTAMP_DELTA + 3;
282 	}
283 
284 	if (timestamp != 0 && (timestamp - old_timestamp) <= TIMESTAMP_DELTA)
285 	{
286 		/* Set green color for started task */
287 		background = (background == NULL) ? g_strdup ("#aed581") : background;
288 		foreground = (foreground == NULL) ? g_strdup ("#000000") : foreground;
289 	}
290 	else
291 	{
292 		/* Reset color */
293 		background = NULL;
294 		foreground = NULL;
295 	}
296 
297 	gtk_list_store_set (GTK_LIST_STORE (model), iter,
298 		XTM_PTV_COLUMN_PPID, task->ppid,
299 		XTM_PTV_COLUMN_STATE, task->state,
300 		XTM_PTV_COLUMN_VSZ, task->vsz,
301 		XTM_PTV_COLUMN_VSZ_STR, vsz,
302 		XTM_PTV_COLUMN_RSS, task->rss,
303 		XTM_PTV_COLUMN_RSS_STR, rss,
304 		XTM_PTV_COLUMN_CPU, (task->cpu_user + task->cpu_system),
305 		XTM_PTV_COLUMN_CPU_STR, cpu,
306 		XTM_PTV_COLUMN_PRIORITY, task->prio,
307 		XTM_PTV_COLUMN_BACKGROUND, background,
308 		XTM_PTV_COLUMN_FOREGROUND, foreground,
309 		XTM_PTV_COLUMN_TIMESTAMP, old_timestamp,
310 		-1);
311 
312 	g_free (background);
313 	g_free (foreground);
314 	g_free (old_state);
315 	g_free (vsz);
316 	g_free (rss);
317 }
318 
319 static gboolean
task_list_find_for_pid(GArray * task_list,GPid pid,Task ** task,guint * idx)320 task_list_find_for_pid (GArray *task_list, GPid pid, Task **task, guint *idx)
321 {
322 	Task *task_tmp, tkey;
323 	tkey.pid = pid;
324 	task_tmp = bsearch(&tkey, task_list->data, task_list->len, sizeof(Task), task_pid_compare_fn);
325 
326 	if (NULL != task)
327 	{
328 		(*task) = task_tmp;
329 	}
330 	if (NULL != idx)
331 	{
332 		if (NULL != task_tmp)
333 		{
334 			(*idx) = (guint)(((size_t)task_tmp - (size_t)task_list->data) / sizeof(Task));
335 		}
336 		else
337 		{
338 			(*idx) = (~((guint)0));
339 		}
340 	}
341 	return ((NULL != task_tmp));
342 }
343 
344 static glong
__current_timestamp(void)345 __current_timestamp (void)
346 {
347 	gint64 tv = g_get_real_time ();
348 	return tv / G_USEC_PER_SEC;
349 }
350 
351 XtmTaskManager *
xtm_task_manager_new(GtkTreeModel * model)352 xtm_task_manager_new (GtkTreeModel *model)
353 {
354 	XtmTaskManager *manager = g_object_new (XTM_TYPE_TASK_MANAGER, NULL);
355 	_xtm_task_manager_set_model (manager, model);
356 	return manager;
357 }
358 
359 void
xtm_task_manager_get_system_info(XtmTaskManager * manager,guint * num_processes,gfloat * cpu,guint64 * memory_used,guint64 * memory_total,guint64 * swap_used,guint64 * swap_total)360 xtm_task_manager_get_system_info (XtmTaskManager *manager, guint *num_processes, gfloat *cpu,
361 				  guint64 *memory_used, guint64 *memory_total,
362 				  guint64 *swap_used, guint64 *swap_total)
363 
364 {
365 	g_return_if_fail (XTM_IS_TASK_MANAGER (manager));
366 
367 	/* Set number of processes */
368 	*num_processes = manager->tasks->len;
369 
370 	/* Set memory and swap usage */
371 	get_memory_usage (&manager->memory_total, &manager->memory_available, &manager->memory_free, &manager->memory_cache, &manager->memory_buffers,
372 			&manager->swap_total, &manager->swap_free);
373 
374 	*memory_used = manager->memory_total - manager->memory_available;
375 	*memory_total = manager->memory_total;
376 	*swap_used = manager->swap_total - manager->swap_free;
377 	*swap_total = manager->swap_total;
378 
379 	/* Set CPU usage */
380 	get_cpu_usage (&manager->cpu_count, &manager->cpu_user, &manager->cpu_system);
381 	*cpu = manager->cpu_user + manager->cpu_system;
382 }
383 
384 void
xtm_task_manager_get_swap_usage(XtmTaskManager * manager,guint64 * swap_free,guint64 * swap_total)385 xtm_task_manager_get_swap_usage (XtmTaskManager *manager, guint64 *swap_free, guint64 *swap_total)
386 {
387 	g_return_if_fail (XTM_IS_TASK_MANAGER (manager));
388 	*swap_free = manager->swap_free;
389 	*swap_total = manager->swap_total;
390 }
391 
392 const GArray *
xtm_task_manager_get_task_list(XtmTaskManager * manager)393 xtm_task_manager_get_task_list (XtmTaskManager *manager)
394 {
395 	g_return_val_if_fail (XTM_IS_TASK_MANAGER (manager), NULL);
396 	xtm_task_manager_update_model (manager);
397 	return manager->tasks;
398 }
399 
400 void
xtm_task_manager_update_model(XtmTaskManager * manager)401 xtm_task_manager_update_model (XtmTaskManager *manager)
402 {
403 	GArray *array;
404 	guint i;
405 	GtkTreeIter iter;
406 	gboolean valid, need_update, update_cmd_line;
407 	glong timestamp;
408 
409 	g_return_if_fail (XTM_IS_TASK_MANAGER (manager));
410 
411 	/* First time fast refresh. */
412 	timestamp = ((manager->tasks->len == 0) ? 0 : __current_timestamp ());
413 
414 	/* Retrieve new task list */
415 	array = g_array_new (FALSE, FALSE, sizeof (Task));
416 	get_task_list (array);
417 
418 	/* Remove terminated tasks, mark to remove, update existing. */
419 	valid = gtk_tree_model_get_iter_first (manager->model, &iter);
420 	while (valid)
421 	{
422 		GPid pid;
423 		gchar *cpu_str;
424 		glong old_timestamp;
425 		gboolean found;
426 		Task *task, *task_new;
427 		GtkTreeIter cur_iter = iter;
428 
429 		valid = gtk_tree_model_iter_next (manager->model, &iter);
430 		gtk_tree_model_get (manager->model, &cur_iter, XTM_PTV_COLUMN_CPU_STR, &cpu_str, XTM_PTV_COLUMN_TIMESTAMP, &old_timestamp, XTM_PTV_COLUMN_PID, &pid, -1);
431 		found = (g_strcmp0 (cpu_str, "-") == 0);
432 		g_free (cpu_str);
433 		if (found)
434 		{
435 			if ((timestamp - old_timestamp) > TIMESTAMP_DELTA) {
436 				G_DEBUG_FMT ("Remove old task %d", pid);
437 				model_remove_tree_iter (manager->model, &cur_iter);
438 			}
439 			continue;
440 		}
441 
442 		/* Looking for new terminated tasks and check updates for existing. */
443 		found = task_list_find_for_pid (manager->tasks, pid, &task, &i);
444 		if (FALSE == found)
445 		{
446 			G_DEBUG_FMT ("Cached task not found for pid: %d", pid);
447 			continue;
448 		}
449 		found = task_list_find_for_pid (array, pid, &task_new, NULL);
450 		if (FALSE == found) /* Mark as removed. */
451 		{
452 			G_DEBUG_FMT ("Task %d '%s' terminated, marking as removed", pid, task->name);
453 			model_mark_tree_iter_as_removed (manager->model, &cur_iter, timestamp);
454 			g_array_remove_index (manager->tasks, i);
455 			continue;
456 		}
457 
458 		/* Task alive, check for update. */
459 		need_update = FALSE;
460 		update_cmd_line = FALSE;
461 
462 		/* Update the model (with the rest) only if needed, this keeps the CPU cool */
463 		if (model_update_forced || 0 != memcmp(task, task_new, sizeof(Task)))
464 		{
465 			need_update = TRUE;
466 			/* Update command name if needed (can happen) */
467 			update_cmd_line = (model_update_forced || g_strcmp0 (task->cmdline, task_new->cmdline));
468 			memcpy(task, task_new, sizeof(Task));
469 		}
470 		else /* Update color if needed */
471 		{
472 			gchar *color;
473 
474 			gtk_tree_model_get (manager->model, &cur_iter, XTM_PTV_COLUMN_BACKGROUND, &color, -1);
475 			if (color != NULL && (timestamp - old_timestamp) > TIMESTAMP_DELTA)
476 			{
477 				need_update = TRUE;
478 				G_DEBUG_FMT ("Remove color from running PID %d", pid);
479 			}
480 			g_free (color);
481 		}
482 
483 		if (need_update)
484 		{
485 			model_update_tree_iter (manager, &cur_iter, timestamp, update_cmd_line, task);
486 		}
487 	}
488 
489 	/* Append started tasks and update existing ones */
490 	for (i = 0; i < array->len; i++)
491 	{
492 		Task *tasktmp = &g_array_index (array, Task, i);
493 
494 		if (task_list_find_for_pid (manager->tasks, tasktmp->pid, NULL, NULL))
495 			continue;
496 		model_add_task (manager, tasktmp, timestamp);
497 		/* XXX: add bininsert() here. */
498 		g_array_append_val (manager->tasks, *tasktmp);
499 		g_array_sort (manager->tasks, task_pid_compare_fn);
500 		G_DEBUG_FMT ("Add new task %d %s", tasktmp->pid, tasktmp->name);
501 	}
502 
503 	g_array_free (array, TRUE);
504 	model_update_forced = FALSE;
505 
506 	return;
507 }
508 
509 
510 gchar *
get_uid_name(guint uid)511 get_uid_name (guint uid)
512 {
513 	int error;
514 	struct passwd *pw = NULL, pwd_buf;
515 	char buf[4096];
516 
517 	memset(buf, 0, sizeof(buf));
518 	error = getpwuid_r(uid, &pwd_buf, buf, sizeof(buf), &pw);
519 
520 	return (g_strdup ((0 == error && pw != NULL) ? pw->pw_name : "nobody"));
521 }
522 
523 gboolean
send_signal_to_pid(GPid pid,gint xtm_signal)524 send_signal_to_pid (GPid pid, gint xtm_signal)
525 {
526 	gint sig;
527 	gint res;
528 	switch (xtm_signal)
529 	{
530 		case XTM_SIGNAL_TERMINATE:
531 			sig = SIGTERM;
532 			break;
533 		case XTM_SIGNAL_STOP:
534 			sig = SIGSTOP;
535 			break;
536 		case XTM_SIGNAL_CONTINUE:
537 			sig = SIGCONT;
538 			break;
539 		case XTM_SIGNAL_KILL:
540 			sig = SIGKILL;
541 			break;
542 		default:
543 			return TRUE;
544 	}
545 	res = kill (pid, sig);
546 	return (res == 0) ? TRUE : FALSE;
547 }
548 
549 gboolean
set_priority_to_pid(GPid pid,gint priority)550 set_priority_to_pid (GPid pid, gint priority)
551 {
552 	gint prio;
553 	gint res;
554 	switch (priority)
555 	{
556 		case XTM_PRIORITY_VERY_LOW:
557 			prio = 15;
558 			break;
559 		case XTM_PRIORITY_LOW:
560 			prio = 5;
561 			break;
562 		case XTM_PRIORITY_NORMAL:
563 			prio = 0;
564 			break;
565 		case XTM_PRIORITY_HIGH:
566 			prio = -5;
567 			break;
568 		case XTM_PRIORITY_VERY_HIGH:
569 			prio = -15;
570 			break;
571 		default:
572 			return TRUE;
573 	}
574 	res = setpriority (PRIO_PROCESS, pid, prio);
575 	return (res == 0) ? TRUE : FALSE;
576 }
577 
578 gint
task_pid_compare_fn(gconstpointer a,gconstpointer b)579 task_pid_compare_fn(gconstpointer a, gconstpointer b)
580 {
581 	return (((const Task*)a)->pid - ((const Task*)b)->pid);
582 }
583