Issues (1839)

html/user/submit_rpc_handler.php (1 issue)

1
<?php
2
// This file is part of BOINC.
3
// http://boinc.berkeley.edu
4
// Copyright (C) 2011 University of California
5
//
6
// BOINC is free software; you can redistribute it and/or modify it
7
// under the terms of the GNU Lesser General Public License
8
// as published by the Free Software Foundation,
9
// either version 3 of the License, or (at your option) any later version.
10
//
11
// BOINC is distributed in the hope that it will be useful,
12
// but WITHOUT ANY WARRANTY; without even the implied warranty of
13
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
14
// See the GNU Lesser General Public License for more details.
15
//
16
// You should have received a copy of the GNU Lesser General Public License
17
// along with BOINC.  If not, see <http://www.gnu.org/licenses/>.
18
19
// Handler for remote job submission.
20
// See https://github.com/BOINC/boinc/wiki/RemoteJobs
21
22
require_once("../inc/boinc_db.inc");
23
require_once("../inc/submit_db.inc");
24
require_once("../inc/xml.inc");
25
require_once("../inc/dir_hier.inc");
26
require_once("../inc/result.inc");
27
require_once("../inc/submit_util.inc");
28
29
ini_set("memory_limit", "4G");
30
31
function get_wu($name) {
32
    $name = BoincDb::escape_string($name);
33
    $wu = BoincWorkunit::lookup("name='$name'");
34
    if (!$wu) {
35
        log_write("no job named $name was found");
36
        xml_error(-1, "no job named $name was found");
37
    }
38
    return $wu;
39
}
40
41
function get_submit_app($name) {
42
    $name = BoincDb::escape_string($name);
43
    $app = BoincApp::lookup("name='$name'");
44
    if (!$app) {
45
        log_write("no app named $name was found");
46
        xml_error(-1, "no app named $name was found");
47
    }
48
    return $app;
49
}
50
51
// estimate FLOP count for a batch.
52
// If estimates aren't included in the job descriptions,
53
// use what's in the input template
54
//
55
function batch_flop_count($r, $template) {
56
    $x = 0;
57
    $t = 0;
58
    if ($template) {
59
        $t = (double)$template->workunit->rsc_fpops_est;
60
    }
61
    foreach($r->batch->job as $job) {
62
        $y = (double)$job->rsc_fpops_est;
63
        if ($y) {
64
            $x += $y;
65
        } else if ($t) {
66
            $x += $t;
67
        } else {
68
            log_write("no rsc_fpops_est given for job");
69
            xml_error(-1, "no rsc_fpops_est given for job");
70
        }
71
    }
72
    return $x;
73
}
74
75
// estimate project FLOPS based on recent average credit
76
//
77
function project_flops() {
78
    $x = BoincUser::sum("expavg_credit");
79
    if ($x == 0) $x = 200;
80
    $y = 1e9*$x/200;
81
    return $y;
82
}
83
84
function est_elapsed_time($r, $template) {
85
    // crude estimate: batch FLOPs / project FLOPS
86
    //
87
    return batch_flop_count($r, $template) / project_flops();
88
}
89
90
// if batch-level input template filename was given, read it;
91
// else if standard file (app_in) is present, read it;
92
// else return null
93
// Note: input templates may also be given per job
94
//
95
function read_input_template($app, $r) {
96
    if ((isset($r->batch)) && (isset($r->batch->workunit_template_file)) && ($r->batch->workunit_template_file)) {
97
        $path = project_dir() . "/templates/".$r->batch->workunit_template_file;
98
    } else {
99
        $path = project_dir() . "/templates/$app->name"."_in";
100
    }
101
    if (file_exists($path)) {
102
        $x = simplexml_load_file($path);
103
        if (!$x) {
104
            log_write("couldn't parse input template file $path");
105
            xml_error(-1, "couldn't parse input template file $path");
106
        }
107
        return $x;
108
    } else {
109
        return null;
110
    }
111
}
112
113
function check_max_jobs_in_progress($r, $user_submit) {
114
    if (!$user_submit->max_jobs_in_progress) return;
115
    $query = "select count(*) as total from DBNAME.result, DBNAME.batch where batch.user_id=$userid and result.batch = batch.id and result.server_state<".RESULT_SERVER_STATE_OVER;
116
    $db = BoincDb::get();
117
    $n = $db->get_int($query, 'total');
118
    if ($n === false) return;
119
    if ($n + count($r->batch->job) > $user_submit->max_jobs_in_progress) {
120
        log_write("limit on jobs in progress exceeded");
121
        xml_error(-1, "limit on jobs in progress exceeded");
122
    }
123
}
124
125
function estimate_batch($r) {
126
    xml_start_tag("estimate_batch");
127
    $app = get_submit_app((string)($r->batch->app_name));
128
    list($user, $user_submit) = check_remote_submit_permissions($r, $app);
129
130
    $template = read_input_template($app, $r);
131
    $e = est_elapsed_time($r, $template);
132
    echo "<seconds>$e</seconds>
133
        </estimate_batch>
134
    ";
135
}
136
137
// Verify that the number of input files for each job agrees with its template
138
// The arg is the batch-level template, if any.
139
// Jobs may have their own templates.
140
//
141
function validate_batch($jobs, $template) {
142
    $i = 0;
143
    $n = count($template->file_info);
144
    foreach($jobs as $job) {
145
        $m = count($job->input_files);
146
        if ($n != $m) {
147
            log_write("wrong # of input files for job $i: need $n, got $m");
148
            xml_error(-1, "wrong # of input files for job $i: need $n, got $m");
149
        }
150
        $i++;
151
    }
152
}
153
154
$fanout = parse_config(get_config(), "<uldl_dir_fanout>");
155
156
// stage a file, and return the physical name
157
//
158
function stage_file($file) {
159
    global $fanout;
160
    $download_dir = parse_config(get_config(), "<download_dir>");
161
162
    switch ($file->mode) {
163
    case "semilocal":
164
    case "local":
165
        // read the file (from disk or network) to get MD5.
166
        // Copy to download hier, using a physical name based on MD5
167
        //
168
        $md5 = md5_file($file->source);
169
        if (!$md5) {
170
            log_write("Can't get MD5 of file $file->source");
171
            xml_error(-1, "Can't get MD5 of file $file->source");
172
        }
173
        $name = job_file_name($md5);
174
        $path = dir_hier_path($name, $download_dir, $fanout);
175
        if (file_exists($path)) return $name;
176
        if (!copy($file->source, $path)) {
177
            log_write("can't copy file from $file->source to $path");
178
            xml_error(-1, "can't copy file from $file->source to $path");
179
        }
180
        return $name;
181
    case "local_staged":
182
        return $file->source;
183
    case "inline":
184
        $md5 = md5($file->source);
185
        if (!$md5) {
186
            log_write("Can't get MD5 of inline data");
187
            xml_error(-1, "Can't get MD5 of inline data");
188
        }
189
        $name = job_file_name($md5);
190
        $path = dir_hier_path($name, $download_dir, $fanout);
191
        if (file_exists($path)) return $name;
192
        if (!file_put_contents($path, $file->source)) {
193
            log_write("can't write to file $path");
194
            xml_error(-1, "can't write to file $path");
195
        }
196
        return $name;
197
    }
198
    log_write(-1, "unsupported file mode: $file->mode");
199
    xml_error(-1, "unsupported file mode: $file->mode");
200
}
201
202
// stage all the files
203
//
204
function stage_files(&$jobs) {
205
    foreach($jobs as $job) {
206
        foreach ($job->input_files as $file) {
207
            if ($file->mode != "remote") {
208
                $file->name = stage_file($file);
209
            }
210
        }
211
    }
212
}
213
214
// submit a list of jobs with a single create_work command.
215
//
216
function submit_jobs(
217
    $jobs, $job_params, $app, $batch_id, $priority, $app_version_num,
218
    $input_template_filename,        // batch-level; can also specify per job
219
    $output_template_filename,
220
    $user
221
) {
222
    global $input_templates, $output_templates;
223
224
    // make a string to pass to create_work;
225
    // one line per job
226
    //
227
    $x = "";
228
    foreach($jobs as $job) {
229
        if ($job->name) {
230
            $x .= " --wu_name $job->name";
231
        }
232
        if ($job->command_line) {
233
            $x .= " --command_line \"$job->command_line\"";
234
        }
235
        if ($job->target_team) {
236
            $x .= " --target_team $job->target_team";
237
        } elseif ($job->target_user) {
238
            $x .= " --target_user $job->target_user";
239
        } elseif ($job->target_host) {
240
            $x .= " --target_host $job->target_host";
241
        }
242
        foreach ($job->input_files as $file) {
243
            if ($file->mode == "remote") {
244
                $x .= " --remote_file $file->url $file->nbytes $file->md5";
245
            } else {
246
                $x .= " $file->name";
247
            }
248
        }
249
        if ($job->input_template) {
250
            $f = $input_templates[$job->input_template_xml];
251
            $x .= " --wu_template $f";
252
        }
253
        if ($job->output_template) {
254
            $f = $output_templates[$job->output_template_xml];
255
            $x .= " --result_template $f";
256
        }
257
        if (isset($job->priority)) {
258
            $x .= " --priority $job->priority";
259
        }
260
        $x .= "\n";
261
    }
262
263
    $cmd = "cd " . project_dir() . "; ./bin/create_work --appname $app->name --batch $batch_id";
264
265
    if ($user->seti_id) {
266
        $cmd .= " --target_user $user->id ";
267
    }
268
    if ($priority !== null) {
269
        $cmd .= " --priority $priority";
270
    }
271
    if ($input_template_filename) {
272
        $cmd .= " --wu_template templates/$input_template_filename";
273
    }
274
    if ($output_template_filename) {
275
        $cmd .= " --result_template templates/$output_template_filename";
276
    }
277
    if ($app_version_num) {
278
        $cmd .= " --app_version_num $app_version_num";
279
    }
280
    if ($job_params->rsc_disk_bound) {
281
        $cmd .= " --rsc_disk_bound $job_params->rsc_disk_bound";
282
    }
283
    if ($job_params->rsc_fpops_est) {
284
        $cmd .= " --rsc_fpops_est $job_params->rsc_fpops_est";
285
    }
286
    if ($job_params->rsc_fpops_bound) {
287
        $cmd .= " --rsc_fpops_bound $job_params->rsc_fpops_bound";
288
    }
289
    if ($job_params->rsc_memory_bound) {
290
        $cmd .= " --rsc_memory_bound $job_params->rsc_memory_bound";
291
    }
292
    if ($job_params->delay_bound) {
293
        $cmd .= " --delay_bound $job_params->delay_bound";
294
    }
295
    $cmd .= " --stdin ";
296
297
    // send stdin/stderr to a temp file
298
    $cmd .= sprintf(' >%s 2>&1',
299
        "/tmp/create_work_" . getmypid() . ".err"
300
    );
301
302
    $h = popen($cmd, "w");
303
    if ($h === false) {
304
        xml_error(-1, "can't run create_work");
305
    }
306
    fwrite($h, $x);
307
    $ret = pclose($h);
308
    if ($ret) {
309
        $err = file_get_contents($errfile);
310
        unlink($errfile);
311
        xml_error(-1, "create_work failed: $err");
312
    }
313
    unlink($errfile);
314
}
315
316
// lists of arrays for job-level templates;
317
// each maps template to filename
318
//
319
$input_templates = array();
320
$output_templates = array();
321
322
// The job specifies an input template.
323
// Check whether the template is already in our map.
324
// If not, write it to a temp file.
325
//
326
function make_input_template($job) {
327
    global $input_templates;
328
    if (!array_key_exists($job->input_template_xml, $input_templates)) {
329
        $f = tempnam("/tmp", "input_template_");
330
        //echo "writing wt $f\n";
331
        file_put_contents($f, $job->input_template_xml);
332
        $input_templates[$job->input_template_xml] = $f;
333
    //} else {
334
    //    echo "dup wu template\n";
335
    }
336
}
337
338
// same for output templates.
339
// A little different because these have to exist for life of job.
340
// Store them in templates/tmp/, with content-based filenames
341
//
342
function make_output_template($job) {
343
    global $output_templates;
344
    if (!array_key_exists($job->output_template_xml, $output_templates)) {
345
        $m = md5($job->output_template_xml);
346
        $filename = "templates/tmp/$m";
347
        $path = "../../$filename";
348
        if (!file_exists($filename)) {
349
            @mkdir("../../templates/tmp");
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for mkdir(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

349
            /** @scrutinizer ignore-unhandled */ @mkdir("../../templates/tmp");

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
350
            file_put_contents($path, $job->output_template_xml);
351
        }
352
        $output_templates[$job->output_template_xml] = $filename;
353
    //} else {
354
    //    echo "dup result template\n";
355
    }
356
}
357
358
// delete per-job WU templates after creating jobs.
359
// (we can't delete result templates)
360
//
361
function delete_input_templates() {
362
    global $input_templates;
363
    foreach ($input_templates as $t => $f) {
364
        unlink($f);
365
    }
366
}
367
368
// convert job list from XML nodes to our own objects
369
//
370
function xml_get_jobs($r) {
371
    $jobs = array();
372
    foreach($r->batch->job as $j) {
373
        $job = new StdClass;
374
        $job->input_files = array();
375
        $job->command_line = (string)$j->command_line;
376
        $job->target_team = (int)$j->target_team;
377
        $job->target_user = (int)$j->target_user;
378
        $job->target_host = (int)$j->target_host;
379
        $job->name = (string)$j->name;
380
        $job->rsc_fpops_est = (double)$j->rsc_fpops_est;
381
        $job->input_template = null;
382
        if ($j->input_template) {
383
            $job->input_template = $j->input_template;
384
            $job->input_template_xml = $j->input_template->asXML();
385
        }
386
        $job->output_template = null;
387
        if ($j->output_template) {
388
            $job->output_template = $j->output_template;
389
            $job->output_template_xml = $j->output_template->asXML();
390
        }
391
        foreach ($j->input_file as $f) {
392
            $file = new StdClass;
393
            $file->mode = (string)$f->mode;
394
            if ($file->mode == "remote") {
395
                $file->url = (string)$f->url;
396
                $file->nbytes = (double)$f->nbytes;
397
                $file->md5 = (string)$f->md5;
398
            } else {
399
                $file->source = (string)$f->source;
400
            }
401
            $job->input_files[] = $file;
402
        }
403
        if (isset($j->priority)) {
404
            $job->priority = (int)$j->priority;
405
        }
406
        $jobs[] = $job;
407
        if ($job->input_template) {
408
            make_input_template($job);
409
        }
410
        if ($job->output_template) {
411
            make_output_template($job);
412
        }
413
    }
414
    return $jobs;
415
}
416
417
// - compute batch FLOP count
418
// - run adjust_user_priorities to increment user_submit.logical_start_time
419
// - return that (use as batch logical end time and job priority)
420
//
421
function logical_end_time($r, $jobs, $user, $app) {
422
    $total_flops = 0;
423
    foreach($jobs as $job) {
424
        //print_r($job);
425
        if ($job->rsc_fpops_est) {
426
            $total_flops += $job->rsc_fpops_est;
427
        } else if ($job->input_template && $job->input_template->workunit->rsc_fpops_est) {
428
            $total_flops += (double) $job->input_template->workunit->rsc_fpops_est;
429
        } else if ($r->batch->job_params->rsc_fpops_est) {
430
            $total_flops += (double) $r->batch->job_params->rsc_fpops_est;
431
        } else {
432
            $x = (double) $template->workunit->rsc_fpops_est;
433
            if ($x) {
434
                $total_flops += $x;
435
            } else {
436
                xml_error(-1, "no rsc_fpops_est given");
437
            }
438
        }
439
    }
440
    $cmd = "cd " . project_dir() . "/bin; ./adjust_user_priority --user $user->id --flops $total_flops --app $app->name";
441
    $x = exec($cmd);
442
    if (!is_numeric($x) || (double)$x == 0) {
443
        xml_error(-1, "$cmd returned $x");
444
    }
445
    return (double)$x;
446
}
447
448
// $r is a simplexml object encoding the request message
449
//
450
function submit_batch($r) {
451
    xml_start_tag("submit_batch");
452
    $app = get_submit_app((string)($r->batch->app_name));
453
    list($user, $user_submit) = check_remote_submit_permissions($r, $app);
454
    $jobs = xml_get_jobs($r);
455
    $template = read_input_template($app, $r);
456
    if ($template) {
457
        validate_batch($jobs, $template);
458
    }
459
    stage_files($jobs);
460
    $njobs = count($jobs);
461
    $now = time();
462
    $app_version_num = (int)($r->batch->app_version_num);
463
464
    // batch may or may not already exist.
465
    // If it does, make sure it's owned by this user
466
    //
467
    $batch_id = (int)($r->batch->batch_id);
468
    if ($batch_id) {
469
        $batch = BoincBatch::lookup_id($batch_id);
470
        if (!$batch) {
471
            log_write("not batch $batch_id");
472
            xml_error(-1, "no batch $batch_id");
473
        }
474
        if ($batch->user_id != $user->id) {
475
            log_write("not owner of batch");
476
            xml_error(-1, "not owner of batch");
477
        }
478
        if ($batch->state != BATCH_STATE_INIT) {
479
            log_write("batch not in init state");
480
            xml_error(-1, "batch not in init state");
481
        }
482
    }
483
484
    // compute a priority for the jobs
485
    //
486
    $priority = null;
487
    $let = 0;
488
    if ($r->batch->allocation_priority) {
489
        $let = logical_end_time($r, $jobs, $user, $app);
490
        $priority = -(int)$let;
491
    } else if (isset($r->batch->priority)) {
492
        $priority = (int)$r->batch->priority;
493
    }
494
495
    if ($batch_id) {
496
        $ret = $batch->update("njobs=$njobs, logical_end_time=$let");
497
        if (!$ret) {
498
            log_write("batch update to njobs failed");
499
            xml_error(-1, "batch->update() failed");
500
        }
501
        log_write("adding jobs to existing batch $batch_id");
502
    } else {
503
        $batch_name = (string)($r->batch->batch_name);
504
        $batch_name = BoincDb::escape_string($batch_name);
505
        $state = BATCH_STATE_INIT;
506
        $batch_id = BoincBatch::insert(
507
            "(user_id, create_time, logical_start_time, logical_end_time, est_completion_time, njobs, fraction_done, nerror_jobs, state, completion_time, credit_estimate, credit_canonical, credit_total, name, app_id, project_state, description, expire_time) values ($user->id, $now, 0, $let, 0, $njobs, 0, 0, $state, 0, 0, 0, 0, '$batch_name', $app->id, 0, '', 0)"
508
        );
509
        if (!$batch_id) {
510
            log_write("can't create batch");
511
            xml_error(-1, "Can't create batch: ".BoincDb::error());
512
        }
513
        $batch = BoincBatch::lookup_id($batch_id);
514
        log_write("created batch $batch_id");
515
    }
516
517
    $job_params = new StdClass;
518
    $job_params->rsc_disk_bound = (double) $r->batch->job_params->rsc_disk_bound;
519
    $job_params->rsc_fpops_est = (double) $r->batch->job_params->rsc_fpops_est;
520
    $job_params->rsc_fpops_bound = (double) $r->batch->job_params->rsc_fpops_bound;
521
    $job_params->rsc_memory_bound = (double) $r->batch->job_params->rsc_memory_bound;
522
    $job_params->delay_bound = (double) $r->batch->job_params->delay_bound;
523
        // could add quorum-related stuff
524
525
    $input_template_filename = (string) $r->batch->input_template_filename;
526
    $output_template_filename = (string) $r->batch->output_template_filename;
527
        // possibly empty
528
529
    submit_jobs(
530
        $jobs, $job_params, $app, $batch_id, $priority, $app_version_num,
531
        $input_template_filename,
532
        $output_template_filename,
533
        $user
534
    );
535
536
    // set state to IN_PROGRESS only after creating jobs;
537
    // otherwise something else might flag batch as COMPLETE
538
    //
539
    $ret = $batch->update("state= ".BATCH_STATE_IN_PROGRESS);
540
    if (!$ret) {
541
        log_write("batch update to IN_PROGRESS failed");
542
        xml_error(-1, "batch->update() failed");
543
    }
544
    log_write("updated batch state to IN_PROGRESS");
545
546
    echo "<batch_id>$batch_id</batch_id>
547
        </submit_batch>
548
    ";
549
550
    delete_input_templates();
551
}
552
553
function create_batch($r) {
554
    xml_start_tag("create_batch");
555
    $app = get_submit_app((string)($r->app_name));
556
    list($user, $user_submit) = check_remote_submit_permissions($r, $app);
557
    $now = time();
558
    $batch_name = (string)($r->batch_name);
559
    $batch_name = BoincDb::escape_string($batch_name);
560
    $expire_time = (double)($r->expire_time);
561
    $state = BATCH_STATE_INIT;
562
    $batch_id = BoincBatch::insert(
563
        "(user_id, create_time, logical_start_time, logical_end_time, est_completion_time, njobs, fraction_done, nerror_jobs, state, completion_time, credit_estimate, credit_canonical, credit_total, name, app_id, project_state, description, expire_time) values ($user->id, $now, 0, 0, 0, 0, 0, 0, $state, 0, 0, 0, 0, '$batch_name', $app->id, 0, '', $expire_time)"
564
    );
565
    if (!$batch_id) {
566
        log_write("Can't create batch: ".BoincDb::error());
567
        xml_error(-1, "Can't create batch: ".BoincDb::error());
568
    }
569
    echo "<batch_id>$batch_id</batch_id>
570
        </create_batch>
571
    ";
572
}
573
574
function print_batch_params($batch, $get_cpu_time) {
575
    $app = BoincApp::lookup_id($batch->app_id);
576
    if (!$app) $app->name = "none";
577
    echo "
578
        <id>$batch->id</id>
579
        <create_time>$batch->create_time</create_time>
580
        <expire_time>$batch->expire_time</expire_time>
581
        <est_completion_time>$batch->est_completion_time</est_completion_time>
582
        <njobs>$batch->njobs</njobs>
583
        <fraction_done>$batch->fraction_done</fraction_done>
584
        <nerror_jobs>$batch->nerror_jobs</nerror_jobs>
585
        <state>$batch->state</state>
586
        <completion_time>$batch->completion_time</completion_time>
587
        <credit_estimate>$batch->credit_estimate</credit_estimate>
588
        <credit_canonical>$batch->credit_canonical</credit_canonical>
589
        <name>$batch->name</name>
590
        <app_name>$app->name</app_name>
591
";
592
    if ($get_cpu_time) {
593
        echo "        <total_cpu_time>".$batch->get_cpu_time()."</total_cpu_time>\n";
594
    }
595
}
596
597
function query_batches($r) {
598
    xml_start_tag("query_batches");
599
    list($user, $user_submit) = check_remote_submit_permissions($r, null);
600
    $batches = BoincBatch::enum("user_id = $user->id");
601
    $get_cpu_time = (int)($r->get_cpu_time);
602
    foreach ($batches as $batch) {
603
        if ($batch->state == BATCH_STATE_RETIRED) continue;
604
        if ($batch->state < BATCH_STATE_COMPLETE) {
605
            $wus = BoincWorkunit::enum("batch = $batch->id");
606
            $batch = get_batch_params($batch, $wus);
607
        }
608
        echo "    <batch>\n";
609
        print_batch_params($batch, $get_cpu_time);
610
        echo "   </batch>\n";
611
    }
612
    echo "</query_batches>\n";
613
}
614
615
function n_outfiles($wu) {
616
    $path = project_dir() . "/$wu->output_template_filename";
617
    $r = simplexml_load_file($path);
618
    return count($r->file_info);
619
}
620
621
// show status of job.
622
// done:
623
// unsent:
624
// in_progress:
625
// error:
626
627
function show_job_details($wu) {
628
    if ($wu->error_mask & WU_ERROR_COULDNT_SEND_RESULT) {
629
        echo "   <error>couldnt_send_result</error>\n";
630
    }
631
    if ($wu->error_mask & WU_ERROR_TOO_MANY_ERROR_RESULTS) {
632
        echo "   <error>too_many_error_results</error>\n";
633
    }
634
    if ($wu->error_mask & WU_ERROR_TOO_MANY_SUCCESS_RESULTS) {
635
        echo "   <error>too_many_success_results</error>\n";
636
    }
637
    if ($wu->error_mask & WU_ERROR_TOO_MANY_TOTAL_RESULTS) {
638
        echo "   <error>too_many_total_results</error>\n";
639
    }
640
    if ($wu->error_mask & WU_ERROR_CANCELLED) {
641
        echo "   <error>cancelled</error>\n";
642
    }
643
    if ($wu->error_mask & WU_ERROR_NO_CANONICAL_RESULT) {
644
        echo "   <error>no_canonical_result</error>\n";
645
    }
646
    $results = BoincResult::enum("workunitid=$wu->id");
647
    $in_progress = 0;
648
    foreach ($results as $r) {
649
        switch ($r->server_state) {
650
        case RESULT_SERVER_STATE_IN_PROGRESS:
651
            $in_progress++;
652
            break;
653
        }
654
        if ($wu->error_mask && ($r->outcome == RESULT_OUTCOME_CLIENT_ERROR)) {
655
            echo "            <exit_status>$r->exit_status</exit_status>\n";
656
        }
657
        if ($r->id == $wu->canonical_resultid) {
658
            echo "            <cpu_time>$r->cpu_time</cpu_time>\n";
659
        }
660
    }
661
    if ($wu->error_mask) {
662
        echo "            <status>error</status>\n";
663
        return;
664
    }
665
666
    if ($wu->canonical_resultid) {
667
        echo "            <status>done</status>\n";
668
    } else {
669
        if ($in_progress > 0) {
670
            echo "            <status>in_progress</status>\n";
671
        } else {
672
            echo "            <status>queued</status>\n";
673
        }
674
    }
675
}
676
677
// return a batch specified by the command, using either ID or name
678
//
679
function get_batch($r) {
680
    $batch = NULL;
681
    if (!empty($r->batch_id)) {
682
        $batch_id = (int)($r->batch_id);
683
        $batch = BoincBatch::lookup_id($batch_id);
684
    } else if (!empty($r->batch_name)) {
685
        $batch_name = (string)($r->batch_name);
686
        $batch_name = BoincDb::escape_string($batch_name);
687
        $batch = BoincBatch::lookup_name($batch_name);
688
    } else {
689
        log_write("batch not specified");
690
        xml_error(-1, "batch not specified");
691
    }
692
    if (!$batch) {
693
        log_write("no such batch");
694
        xml_error(-1, "no such batch");
695
    }
696
    return $batch;
697
}
698
699
function query_batch($r) {
700
    xml_start_tag("query_batch");
701
    list($user, $user_submit) = check_remote_submit_permissions($r, null);
702
    $batch = get_batch($r);
703
    if ($batch->user_id != $user->id) {
704
        log_write("not owner of batch");
705
        xml_error(-1, "not owner of batch");
706
    }
707
708
    $wus = BoincWorkunit::enum("batch = $batch->id", "order by id");
709
    $batch = get_batch_params($batch, $wus);
710
    $get_cpu_time = (int)($r->get_cpu_time);
711
    $get_job_details = (int)($r->get_job_details);
712
    print_batch_params($batch, $get_cpu_time);
713
    foreach ($wus as $wu) {
714
        echo "        <job>
715
        <id>$wu->id</id>
716
        <name>$wu->name</name>
717
        <canonical_instance_id>$wu->canonical_resultid</canonical_instance_id>
718
";
719
        // does anyone need this?
720
        //
721
        if (0) {
722
            $n_outfiles = n_outfiles($wu);
723
            echo "     <n_outfiles>$n_outfiles</n_outfiles>\n";
724
        }
725
726
        if ($get_job_details) {
727
            show_job_details($wu);
728
        }
729
        echo "        </job>\n";
730
    }
731
    echo "</query_batch>\n";
732
}
733
734
function results_sent($wu) {
735
    return BoincResult::count("workunitid=$wu->id and sent_time>0");
736
}
737
738
// variant for Condor, which doesn't care about job instances
739
// and refers to batches by name
740
//
741
function query_batch2($r) {
742
    xml_start_tag("query_batch2");
743
    list($user, $user_submit) = check_remote_submit_permissions($r, null);
744
    $batch_names = $r->batch_name;
745
    $batches = array();
746
    foreach ($batch_names as $b) {
747
        $batch_name = (string)$b;
748
        $batch_name = BoincDb::escape_string($batch_name);
749
        $batch = BoincBatch::lookup_name($batch_name);
750
        if (!$batch) {
751
            log_write("no batch named $batch_name");
752
            xml_error(-1, "no batch named $batch_name");
753
        }
754
        if ($batch->user_id != $user->id) {
755
            log_write("not owner of $batch_name");
756
            xml_error(-1, "not owner of $batch_name");
757
        }
758
        $batches[] = $batch;
759
    }
760
761
    $min_mod_time = (double)$r->min_mod_time;
762
    if ($min_mod_time) {
763
        $mod_time_clause = "and mod_time > FROM_UNIXTIME($min_mod_time)";
764
    } else {
765
        $mod_time_clause = "";
766
    }
767
768
    $t = dtime();
769
    echo "<server_time>$t</server_time>\n";
770
    foreach ($batches as $batch) {
771
        $wus = BoincWorkunit::enum("batch = $batch->id $mod_time_clause");
772
        echo "   <batch_size>".count($wus)."</batch_size>\n";
773
774
        // job status is:
775
        // DONE if done
776
        // ERROR if error
777
        // IN_PROGRESS if at least one instance sent
778
        // QUEUED if no instances sent
779
        foreach ($wus as $wu) {
780
            if ($wu->canonical_resultid) {
781
                $status = "DONE";
782
            } else if ($wu->error_mask) {
783
                $status = "ERROR";
784
            } else if (results_sent($wu) > 0) {
785
                $status = "IN_PROGRESS";
786
            } else {
787
                $status = "UNSENT";
788
            }
789
            echo
790
"    <job>
791
        <job_name>$wu->name</job_name>
792
        <status>$status</status>
793
    </job>
794
";
795
        }
796
    }
797
    echo "</query_batch2>\n";
798
}
799
800
function query_job($r) {
801
    xml_start_tag("query_job");
802
    list($user, $user_submit) = check_remote_submit_permissions($r, null);
803
    $job_id = (int)($r->job_id);
804
    $wu = BoincWorkunit::lookup_id($job_id);
805
    if (!$wu) {
806
        log_write("no such job");
807
        xml_error(-1, "no such job");
808
    }
809
    $batch = BoincBatch::lookup_id($wu->batch);
810
    if ($batch->user_id != $user->id) {
811
        log_write("not owner");
812
        xml_error(-1, "not owner");
813
    }
814
    $results = BoincResult::enum("workunitid=$job_id");
815
    foreach ($results as $result) {
816
        echo "    <instance>
817
        <name>$result->name</name>
818
        <id>$result->id</id>
819
        <state>".state_string($result)."</state>
820
";
821
        if ($result->server_state == 5) {   // over?
822
            $paths = get_outfile_paths($result);
823
            foreach($paths as $path) {
824
                if (is_file($path)) {
825
                    $size = filesize($path);
826
                    echo "        <outfile>
827
            <size>$size</size>
828
        </outfile>
829
";
830
                }
831
            }
832
        }
833
        echo "</instance>\n";
834
    }
835
    echo "</query_job>\n";
836
}
837
838
// the following for Condor.
839
// If the job has a canonical instance, return info about it.
840
// Otherwise find an instance that completed
841
// (possibly crashed) and return its info.
842
//
843
function query_completed_job($r) {
844
    xml_start_tag("query_completed_job");
845
    list($user, $user_submit) = check_remote_submit_permissions($r, null);
846
    $job_name = (string)($r->job_name);
847
    $job_name = BoincDb::escape_string($job_name);
848
    $wu = BoincWorkunit::lookup("name='$job_name'");
849
    if (!$wu) {
850
        log_write("no such job");
851
        xml_error(-1, "no such job");
852
    }
853
    $batch = BoincBatch::lookup_id($wu->batch);
854
    if ($batch->user_id != $user->id) {
855
        log_write("not owner");
856
        xml_error(-1, "not owner");
857
    }
858
859
    echo "<completed_job>\n";
860
    echo "   <error_mask>$wu->error_mask</error_mask>\n";
861
    if ($wu->canonical_resultid) {
862
        $result = BoincResult::lookup_id($wu->canonical_resultid);
863
        echo "   <canonical_resultid>$wu->canonical_resultid</canonical_resultid>\n";
864
    } else {
865
        $results = BoincResult::enum("workunitid=$job_id");
866
        foreach ($results as $r) {
867
            switch($r->outcome) {
868
            case 1:
869
            case 3:
870
            case 6:
871
                $result = $r;
872
                break;
873
            }
874
        }
875
        if ($result) {
876
            echo "   <error_resultid>$result->id</error_resultid>\n";
877
        }
878
    }
879
    if ($result) {
880
        echo "   <exit_status>$result->exit_status</exit_status>\n";
881
        echo "   <elapsed_time>$result->elapsed_time</elapsed_time>\n";
882
        echo "   <cpu_time>$result->cpu_time</cpu_time>\n";
883
        echo "   <stderr_out><![CDATA[\n";
884
        echo htmlspecialchars($result->stderr_out);
885
        echo "   ]]></stderr_out>\n";
886
    }
887
    echo "</completed_job>
888
        </query_completed_job>
889
    ";
890
}
891
892
function handle_abort_batch($r) {
893
    xml_start_tag("abort_batch");
894
    list($user, $user_submit) = check_remote_submit_permissions($r, null);
895
    $batch = get_batch($r);
896
    if ($batch->user_id != $user->id) {
897
        log_write("not owner");
898
        xml_error(-1, "not owner");
899
    }
900
    abort_batch($batch);
901
    echo "<success>1</success>
902
        </abort_batch>
903
    ";
904
}
905
906
// handle the abort of jobs possibly belonging to different batches
907
//
908
function handle_abort_jobs($r) {
909
    xml_start_tag("abort_jobs");
910
    list($user, $user_submit) = check_remote_submit_permissions($r, null);
911
    $batch = null;
912
    foreach ($r->job_name as $job_name) {
913
        $job_name = BoincDb::escape_string($job_name);
914
        $wu = BoincWorkunit::lookup("name='$job_name'");
915
        if (!$wu) {
916
            log_write("no job $job_name");
917
            xml_error(-1, "no job $job_name");
918
        }
919
        if (!$wu->batch) {
920
            log_write("job $job_name is not part of a batch");
921
            xml_error(-1, "job $job_name is not part of a batch");
922
        }
923
        if (!$batch || $wu->batch != $batch->id) {
924
            $batch = BoincBatch::lookup_id($wu->batch);
925
        }
926
        if (!$batch || $batch->user_id != $user->id) {
927
            log_write("not owner of batch");
928
            xml_error(-1, "not owner of batch");
929
        }
930
        echo "<aborted $job_name>\n";
931
        abort_workunit($wu);
932
    }
933
    echo "<success>1</success>
934
        </abort_jobs>
935
    ";
936
}
937
938
function handle_retire_batch($r) {
939
    xml_start_tag("retire_batch");
940
    list($user, $user_submit) = check_remote_submit_permissions($r, null);
941
    $batch = get_batch($r);
942
    if ($batch->user_id != $user->id) {
943
        log_write("not owner of batch");
944
        xml_error(-1, "not owner of batch");
945
    }
946
    retire_batch($batch);
947
    echo "<success>1</success>
948
        </retire_batch>
949
    ";
950
}
951
952
function handle_set_expire_time($r) {
953
    xml_start_tag("set_expire_time");
954
    list($user, $user_submit) = check_remote_submit_permissions($r, null);
955
    $batch = get_batch($r);
956
    if ($batch->user_id != $user->id) {
957
        log_write("not owner of batch");
958
        xml_error(-1, "not owner of batch");
959
    }
960
    $expire_time = (double)($r->expire_time);
961
    if ($batch->update("expire_time=$expire_time")) {
962
        echo "<success>1</success>";
963
    } else {
964
        log_write("batch update failed");
965
        xml_error(-1, "batch update failed");
966
    }
967
    echo "</set_expire_time>\n";
968
}
969
970
function get_templates($r) {
971
    xml_start_tag("get_templates");
972
    $app_name = (string)($r->app_name);
973
    if ($app_name) {
974
        $app = get_submit_app($app_name);
975
    } else {
976
        $job_name = (string)($r->job_name);
977
        $wu = get_wu($job_name);
978
        $app = BoincApp::lookup_id($wu->appid);
979
    }
980
981
    list($user, $user_submit) = check_remote_submit_permissions($r, $app);
982
    $in = file_get_contents(project_dir() . "/templates/".$app->name."_in");
983
    $out = file_get_contents(project_dir() . "/templates/".$app->name."_out");
984
    if ($in === false || $out === false) {
985
        log_write("template file missing");
986
        xml_error(-1, "template file missing");
987
    }
988
    echo "<templates>\n$in\n$out\n</templates>
989
        </get_templates>
990
    ";
991
}
992
993
function ping($r) {
994
    xml_start_tag("ping");
995
    BoincDb::get();     // errors out if DB down or web disabled
996
    echo "<success>1</success>
997
        </ping>
998
    ";
999
}
1000
1001
if (0) {
1002
$r = simplexml_load_string("
1003
<query_batch2>
1004
    <authenticator>x</authenticator>
1005
    <batch_name>batch_30</batch_name>
1006
    <batch_name>batch_31</batch_name>
1007
</query_batch2>
1008
");
1009
query_batch2($r);
1010
exit;
1011
}
1012
1013
if (0) {
1014
$r = simplexml_load_string("
1015
<query_batch>
1016
    <authenticator>x</authenticator>
1017
    <batch_id>54</batch_id>
1018
</query_batch>
1019
");
1020
query_batch($r);
1021
exit;
1022
}
1023
1024
if (0) {
1025
$r = simplexml_load_string("
1026
<query_job>
1027
    <authenticator>x</authenticator>
1028
    <job_id>312173</job_id>
1029
</query_job>
1030
");
1031
query_job($r);
1032
exit;
1033
}
1034
1035
if (0) {
1036
$r = simplexml_load_string("
1037
<estimate_batch>
1038
    <authenticator>x</authenticator>
1039
    <batch>
1040
    <app_name>remote_test</app_name>
1041
    <batch_name>Aug 6 batch 4</batch_name>
1042
    <job>
1043
        <rsc_fpops_est>19000000000</rsc_fpops_est>
1044
        <command_line>--t 19</command_line>
1045
        <input_file>
1046
            <mode>remote</mode>
1047
            <source>https://google.com/</source>
1048
        </input_file>
1049
    </job>
1050
    </batch>
1051
</estimate_batch>
1052
");
1053
estimate_batch($r);
1054
exit;
1055
}
1056
1057
// optionally write request message (XML) to log file
1058
//
1059
$request_log = parse_config(get_config(), "<remote_submit_request_log>");
1060
if ($request_log) {
1061
    $log_dir = parse_config(get_config(), "<log_dir>");
1062
    $request_log = $log_dir . "/" . $request_log;
1063
    if ($file = fopen($request_log, "a")) {
1064
        fwrite($file, "\n<submit_rpc_handler date=\"" . date(DATE_ATOM) . "\">\n" . $_POST['request'] . "\n</submit_rpc_handler>\n");
1065
        fclose($file);
1066
    }
1067
}
1068
1069
xml_header();
1070
if (0) {
1071
    $req = file_get_contents("submit_req.xml");
1072
} else {
1073
    $req = $_POST['request'];
1074
}
1075
$r = simplexml_load_string($req);
1076
if (!$r) {
1077
    log_write("----- RPC request: can't parse request message: $req");
1078
    xml_error(-1, "can't parse request message: $req");
1079
}
1080
1081
log_write("----- Handling RPC; command ".$r->getName());
1082
1083
switch ($r->getName()) {
1084
    case 'abort_batch': handle_abort_batch($r); break;
1085
    case 'abort_jobs': handle_abort_jobs($r); break;
1086
    case 'create_batch': create_batch($r); break;
1087
    case 'estimate_batch': estimate_batch($r); break;
1088
    case 'get_templates': get_templates($r); break;
1089
    case 'ping': ping($r); break;
1090
    case 'query_batch': query_batch($r); break;
1091
    case 'query_batch2': query_batch2($r); break;
1092
    case 'query_batches': query_batches($r); break;
1093
    case 'query_job': query_job($r); break;
1094
    case 'query_completed_job': query_completed_job($r); break;
1095
    case 'retire_batch': handle_retire_batch($r); break;
1096
    case 'set_expire_time': handle_set_expire_time($r); break;
1097
    case 'submit_batch': submit_batch($r); break;
1098
    default:
1099
        log_write("bad command");
1100
        xml_error(-1, "bad command: ".$r->getName());
1101
        break;
1102
}
1103
1104
log_write("RPC done");
1105
1106
?>
1107