1/*
2 * Copyright 2017 Haiku Inc. All rights reserved.
3 * Distributed under the terms of the MIT License.
4 *
5 * Authors:
6 *		Brian Hill
7 */
8
9
10#include "TaskLooper.h"
11
12#include <Catalog.h>
13#include <MessageQueue.h>
14#include <package/AddRepositoryRequest.h>
15#include <package/DropRepositoryRequest.h>
16#include <package/RefreshRepositoryRequest.h>
17#include <package/PackageRoster.h>
18#include <package/RepositoryConfig.h>
19
20#include "constants.h"
21
22#define DEBUGTASK 0
23
24#undef B_TRANSLATION_CONTEXT
25#define B_TRANSLATION_CONTEXT "TaskLooper"
26
27static const BString kLogResultIndicator = "***";
28static const BString kCompletedText =
29	B_TRANSLATE_COMMENT("Completed", "Completed task status message");
30static const BString kFailedText =
31	B_TRANSLATE_COMMENT("Failed", "Failed task status message");
32static const BString kAbortedText =
33	B_TRANSLATE_COMMENT("Aborted", "Aborted task status message");
34static const BString kDescriptionText =
35	B_TRANSLATE_COMMENT("Description", "Failed task error description");
36static const BString kDetailsText =
37	B_TRANSLATE_COMMENT("Details", "Job log details header");
38
39using BSupportKit::BJob;
40
41
42void
43JobStateListener::JobStarted(BJob* job)
44{
45	fJobLog.Add(job->Title());
46}
47
48
49void
50JobStateListener::JobSucceeded(BJob* job)
51{
52	BString resultText(kLogResultIndicator);
53	fJobLog.Add(resultText.Append(kCompletedText));
54}
55
56
57void
58JobStateListener::JobFailed(BJob* job)
59{
60	BString resultText(kLogResultIndicator);
61	resultText.Append(kFailedText).Append(": ")
62		.Append(strerror(job->Result()));
63	fJobLog.Add(resultText);
64	if (job->ErrorString().Length() > 0) {
65		resultText.SetTo(kLogResultIndicator);
66		resultText.Append(kDescriptionText).Append(": ")
67			.Append(job->ErrorString());
68		fJobLog.Add(resultText);
69	}
70}
71
72
73void
74JobStateListener::JobAborted(BJob* job)
75{
76	BString resultText(kLogResultIndicator);
77	resultText.Append(kAbortedText).Append(": ")
78		.Append(strerror(job->Result()));
79	fJobLog.Add(resultText);
80	if (job->ErrorString().Length() > 0) {
81		resultText.SetTo(kLogResultIndicator);
82		resultText.Append(kDescriptionText).Append(": ")
83			.Append(job->ErrorString());
84		fJobLog.Add(resultText);
85	}
86}
87
88
89BString
90JobStateListener::GetJobLog()
91{
92	return fJobLog.Join("\n");
93}
94
95
96TaskLooper::TaskLooper(const BMessenger& target)
97	:
98	BLooper("TaskLooper"),
99	fReplyTarget(target)
100{
101	Run();
102	fMessenger.SetTo(this);
103}
104
105
106bool
107TaskLooper::QuitRequested()
108{
109	return MessageQueue()->IsEmpty();
110}
111
112
113void
114TaskLooper::MessageReceived(BMessage* message)
115{
116	switch (message->what)
117	{
118		case DO_TASK:
119		{
120			RepoRow* rowItem;
121			status_t result = message->FindPointer(key_rowptr, (void**)&rowItem);
122			if (result == B_OK) {
123				// Check to make sure there isn't already an existing task for this
124				int16 queueCount = fTaskQueue.CountItems();
125				for (int16 index = 0; index<queueCount; index++) {
126					Task* task = fTaskQueue.ItemAt(index);
127					if (rowItem == task->rowItem)
128						break;
129				}
130
131				// Initialize task
132				Task* newTask = new Task();
133				newTask->rowItem = rowItem;
134				newTask->name = rowItem->Name();
135				newTask->resultName = newTask->name;
136				if (rowItem->IsEnabled()) {
137					newTask->taskType = DISABLE_REPO;
138					newTask->taskParam = newTask->name;
139				} else {
140					newTask->taskType = ENABLE_REPO;
141					newTask->taskParam = rowItem->Url();
142				}
143				newTask->owner = this;
144				newTask->fTimer = NULL;
145
146				// Add to queue and start
147				fTaskQueue.AddItem(newTask);
148				BString threadName(newTask->taskType == ENABLE_REPO ?
149					"enable_task" : "disable_task");
150				newTask->threadId = spawn_thread(_DoTask, threadName.String(),
151					B_NORMAL_PRIORITY, (void*)newTask);
152				status_t threadResult;
153				if (newTask->threadId < B_OK)
154					threadResult = B_ERROR;
155				else {
156					threadResult = resume_thread(newTask->threadId);
157					if (threadResult == B_OK) {
158						newTask->fTimer = new TaskTimer(fMessenger, newTask);
159						newTask->fTimer->Start(newTask->name);
160						// Reply to view
161						BMessage reply(*message);
162						reply.what = TASK_STARTED;
163						reply.AddInt16(key_count, fTaskQueue.CountItems());
164						fReplyTarget.SendMessage(&reply);
165					} else
166						kill_thread(newTask->threadId);
167				}
168				if (threadResult != B_OK) {
169					_RemoveAndDelete(newTask);
170				}
171			}
172			break;
173		}
174
175		case TASK_COMPLETED:
176		case TASK_COMPLETED_WITH_ERRORS:
177		case TASK_CANCELED:
178		{
179			Task* task;
180			status_t result = message->FindPointer(key_taskptr, (void**)&task);
181			if (result == B_OK && fTaskQueue.HasItem(task)) {
182				task->fTimer->Stop(task->resultName);
183				BMessage reply(message->what);
184				reply.AddInt16(key_count, fTaskQueue.CountItems()-1);
185				reply.AddPointer(key_rowptr, task->rowItem);
186				if (message->what == TASK_COMPLETED_WITH_ERRORS)
187					reply.AddString(key_details, task->resultErrorDetails);
188				if (task->taskType == ENABLE_REPO
189					&& task->name.Compare(task->resultName) != 0) {
190					reply.AddString(key_name, task->resultName);
191				}
192				fReplyTarget.SendMessage(&reply);
193				_RemoveAndDelete(task);
194			}
195			break;
196		}
197
198		case TASK_KILL_REQUEST:
199		{
200			Task* task;
201			status_t result = message->FindPointer(key_taskptr, (void**)&task);
202			if (result == B_OK && fTaskQueue.HasItem(task)) {
203				kill_thread(task->threadId);
204				BMessage reply(TASK_CANCELED);
205				reply.AddInt16(key_count, fTaskQueue.CountItems()-1);
206				reply.AddPointer(key_rowptr, task->rowItem);
207				fReplyTarget.SendMessage(&reply);
208				_RemoveAndDelete(task);
209			}
210			break;
211		}
212	}
213}
214
215
216void
217TaskLooper::_RemoveAndDelete(Task* task)
218{
219	fTaskQueue.RemoveItem(task);
220	if (task->fTimer) {
221		task->fTimer->Lock();
222		task->fTimer->Quit();
223		task->fTimer = NULL;
224	}
225	delete task;
226}
227
228
229status_t
230TaskLooper::_DoTask(void* data)
231{
232	Task* task = (Task*)data;
233	BString errorDetails, repoName("");
234	status_t returnResult = B_OK;
235	DecisionProvider decisionProvider;
236	JobStateListener listener;
237	switch (task->taskType)
238	{
239		case DISABLE_REPO:
240		{
241			BString nameParam(task->taskParam);
242			BPackageKit::BContext context(decisionProvider, listener);
243			BPackageKit::DropRepositoryRequest dropRequest(context, nameParam);
244			status_t result = dropRequest.Process();
245			if (result != B_OK) {
246				returnResult = result;
247				if (result != B_CANCELED) {
248					errorDetails.Append(B_TRANSLATE_COMMENT("There was an "
249						"error disabling the repository %name%",
250						"Error message, do not translate %name%"));
251					BString nameString("\"");
252					nameString.Append(nameParam).Append("\"");
253					errorDetails.ReplaceFirst("%name%", nameString);
254					_AppendErrorDetails(errorDetails, &listener);
255				}
256			}
257			break;
258		}
259
260		case ENABLE_REPO:
261		{
262			BString urlParam(task->taskParam);
263			BPackageKit::BContext context(decisionProvider, listener);
264			// Add repository
265			bool asUserRepository = false;
266				// TODO does this ever change?
267			BPackageKit::AddRepositoryRequest addRequest(context, urlParam,
268				asUserRepository);
269			status_t result = addRequest.Process();
270			if (result != B_OK) {
271				returnResult = result;
272				if (result != B_CANCELED) {
273					errorDetails.Append(B_TRANSLATE_COMMENT("There was an "
274						"error enabling the repository %url%",
275						"Error message, do not translate %url%"));
276					errorDetails.ReplaceFirst("%url%", urlParam);
277					_AppendErrorDetails(errorDetails, &listener);
278				}
279				break;
280			}
281			// Continue on to refresh repo cache
282			repoName = addRequest.RepositoryName();
283			BPackageKit::BPackageRoster roster;
284			BPackageKit::BRepositoryConfig repoConfig;
285			roster.GetRepositoryConfig(repoName, &repoConfig);
286			BPackageKit::BRefreshRepositoryRequest refreshRequest(context,
287				repoConfig);
288			result = refreshRequest.Process();
289			if (result != B_OK) {
290				returnResult = result;
291				if (result != B_CANCELED) {
292					errorDetails.Append(B_TRANSLATE_COMMENT("There was an "
293						"error refreshing the repository cache for %name%",
294						"Error message, do not translate %name%"));
295					BString nameString("\"");
296					nameString.Append(repoName).Append("\"");
297					errorDetails.ReplaceFirst("%name%", nameString);
298					_AppendErrorDetails(errorDetails, &listener);
299				}
300			}
301			break;
302		}
303	}
304	// Report completion status
305	BMessage reply;
306	if (returnResult == B_OK) {
307		reply.what = TASK_COMPLETED;
308		// Add the repo name if we need to update the list row value
309		if (task->taskType == ENABLE_REPO)
310			task->resultName = repoName;
311	} else if (returnResult == B_CANCELED)
312		reply.what = TASK_CANCELED;
313	else {
314		reply.what = TASK_COMPLETED_WITH_ERRORS;
315		task->resultErrorDetails = errorDetails;
316		if (task->taskType == ENABLE_REPO)
317			task->resultName = repoName;
318	}
319	reply.AddPointer(key_taskptr, task);
320	task->owner->PostMessage(&reply);
321#if DEBUGTASK
322	if (returnResult == B_OK || returnResult == B_CANCELED) {
323		BString degubDetails("Debug info:\n");
324		degubDetails.Append(listener.GetJobLog());
325		(new BAlert("debug", degubDetails, "OK"))->Go(NULL);
326	}
327#endif // DEBUGTASK
328	return 0;
329}
330
331
332void
333TaskLooper::_AppendErrorDetails(BString& details, JobStateListener* listener)
334{
335	details.Append("\n\n").Append(kDetailsText).Append(":\n");
336	details.Append(listener->GetJobLog());
337}
338