create_jobs()   B
last analyzed

Complexity

Conditions 10
Paths 144

Size

Total Lines 56
Code Lines 39

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 10
eloc 39
c 0
b 0
f 0
nc 144
nop 11
dl 0
loc 56
rs 7.2999

How to fix   Long Method    Complexity    Many Parameters   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
2
// This file is part of BOINC.
3
// https://boinc.berkeley.edu
4
// Copyright (C) 2024 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
// web interface for submitting BUDA jobs
20
21
require_once('../inc/util.inc');
22
require_once('../inc/submit_util.inc');
23
require_once('../inc/sandbox.inc');
24
require_once('../inc/buda.inc');
25
require_once('../inc/kw_prefs.inc');
26
27
define('AVG_CPU_FPOPS', 4.3e9);
28
29
display_errors();
30
31
function submit_form($user) {
32
    $app = get_str('app');
33
    if (!is_valid_filename($app)) die('bad arg');
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
34
    $app_desc = get_buda_app_desc($app);
35
    if (!user_can_submit($user, $app_desc)) {
36
        error_page('no permission');
37
    }
38
39
    $sbitems_zip = sandbox_select_items($user, '/.zip$/');
40
    if (!$sbitems_zip) {
41
        error_page("No .zip files in your sandbox.");
42
    }
43
44
    $desc = "<br><small>
45
        A zip file with one directory per job.
46
        Each directory contains the input file(s) for that job
47
        and an optional file <code>cmdline</code>
48
        containing command-line arguments.
49
        <a href=https://github.com/BOINC/boinc/wiki/BUDA-job-submission>Details</a></small>.
50
    ";
51
    page_head("BUDA: Submit jobs to $app");
52
53
    $us = BoincUserSubmit::lookup_userid($user->id);
54
    if ($us->max_jobs_in_progress) {
55
        $n = n_jobs_in_progress($user->id);
56
        echo sprintf(
57
            '<p>Note: you are limited to %d jobs in progress,
58
            and you currently have %d,
59
            so this batch can be at most %d jobs.</p>',
60
            $us->max_jobs_in_progress, $n,
61
            $us->max_jobs_in_progress - $n
62
        );
63
    }
64
    form_start('buda_submit.php');
65
    form_input_hidden('action', 'submit');
66
    form_input_hidden('app', $app);
67
    form_select("Batch zip file $desc", 'batch_file', $sbitems_zip);
68
    form_input_text(
69
        'Command-line arguments<br><small>Passed to all jobs in the batch</small>',
70
        'cmdline'
71
    );
72
    form_input_text(
73
        'Max job runtime (days) on a typical (4.3 GFLOPS) computer.
74
            <br><small>
75
            The runtime limit will be scaled for faster/slower computers.
76
            <br>
77
            Jobs that reach this limit will be aborted.
78
            </small>'
79
        ,
80
        'max_runtime_days', 1
81
    );
82
    form_input_text(
83
        'Expected job runtime (days) on a typical (4.3 GFLOPS) computer.
84
            <br><small>
85
            This determines how many jobs are sent to each host,
86
            and how "fraction done" is computed.
87
            </small>
88
        ',
89
        'exp_runtime_days', .5
90
    );
91
    form_checkbox(
92
        "Enable debugging output <br><small>Write Docker commands and output to stderr. Not recommended for long-running jobs.</small>.",
93
        'wrapper_verbose'
94
    );
95
    form_submit('OK');
96
    form_end();
97
    page_tail();
98
}
99
100
// unzip batch file into a temp dir; return dir name
101
//
102
function unzip_batch_file($user, $batch_file) {
103
    @mkdir("../../buda_batches");
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

103
    /** @scrutinizer ignore-unhandled */ @mkdir("../../buda_batches");

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...
104
    for ($i=0; $i<1000; $i++) {
105
        $batch_dir = "../../buda_batches/$i";
106
        $batch_dir_name = $i;
107
        $ret = @mkdir($batch_dir);
108
        if ($ret) break;
109
    }
110
    if (!$ret) error_page("can't create batch dir");
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $ret does not seem to be defined for all execution paths leading up to this point.
Loading history...
111
    $sb_dir = sandbox_dir($user);
112
    if (!file_exists("$sb_dir/$batch_file")) {
113
        error_page("no batch file $batch_file");
114
    }
115
    system("cd $batch_dir; unzip $sb_dir/$batch_file > /dev/null", $ret);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $batch_dir does not seem to be defined for all execution paths leading up to this point.
Loading history...
116
    if ($ret) {
117
        error_page("unzip error: $ret");
118
    }
119
    return $batch_dir_name;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $batch_dir_name does not seem to be defined for all execution paths leading up to this point.
Loading history...
120
}
121
122
// Scan a batch dir.
123
// Check its validity:
124
// - optional dir 'shared_input_files' has shared input files
125
// - other dirs (job dirs) can have only remaining infiles and possibly cmdline
126
//
127
// Return a structure describing its contents, and the md5/size of files
128
//
129
function parse_batch_dir($batch_dir, $app_desc) {
130
    $input_files = $app_desc->input_file_names;
131
    sort($input_files);
132
    $shared_files = [];
133
    $shared_file_infos = [];
134
    if (is_dir("$batch_dir/shared_input_files")) {
135
        foreach (scandir("$batch_dir/shared_input_files") as $fname) {
136
            if ($fname[0] == '.') continue;
137
            if (!in_array($fname, $input_files)) {
138
                error_page("$fname is not an input file name");
139
            }
140
            $shared_files[] = $fname;
141
            $shared_file_infos[] = get_file_info("$batch_dir/shared_input_files/$fname");
142
        }
143
    }
144
    $unshared_files = array_diff($input_files, $shared_files);
145
    sort($unshared_files);
146
    $jobs = [];
147
    foreach (scandir($batch_dir) as $fname) {
148
        if ($fname[0] == '.') continue;
149
        if ($fname == 'shared_input_files') continue;
150
        if (!is_dir("$batch_dir/$fname")) {
151
            error_page("$batch_dir/$fname is not a directory");
152
        }
153
        $job_files = [];
154
        $cmdline = '';
155
        foreach(scandir("$batch_dir/$fname") as $f2) {
156
            if ($f2[0] == '.') continue;
157
            if ($f2 == 'cmdline') {
158
                $cmdline = trim(file_get_contents("$batch_dir/$fname/cmdline"));
159
                continue;
160
            }
161
            if (!in_array($f2, $unshared_files)) {
162
                error_page("$fname/$f2 is not an input file name");
163
            }
164
            $job_files[] = $f2;
165
        }
166
        if (array_values($job_files) != array_values($unshared_files)) {
167
            error_page("$fname doesn't have all input files");
168
        }
169
170
        //if (!$cmdline && !$job_files) {
171
        //    error_page("job $fname has no cmdline and no input files");
172
        //}
173
174
        $file_infos = [];
175
        foreach ($unshared_files as $f2) {
176
            $file_infos[] = get_file_info("$batch_dir/$fname/$f2");
177
        }
178
179
        $job = new StdClass;
180
        $job->dir = $fname;
181
        $job->cmdline = $cmdline;
182
        $job->file_infos = $file_infos;
183
        $jobs[] = $job;
184
    }
185
    $batch_desc = new StdClass;
186
    $batch_desc->shared_files = $shared_files;
187
    $batch_desc->shared_file_infos = $shared_file_infos;
188
    $batch_desc->unshared_files = $unshared_files;
189
    $batch_desc->jobs = $jobs;
190
    return $batch_desc;
191
}
192
193
function create_batch($user, $njobs, $app) {
194
    global $buda_boinc_app;
195
    $now = time();
196
    $batch_name = sprintf('buda_%d_%d', $user->id, $now);
197
    $description = "$app";
198
    $batch_id = BoincBatch::insert(sprintf(
199
        "(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 (%d, %d, 0, 0, 0, %d, 0, 0, %d, 0, 0, 0, 0, '%s', %d, 0, '%s', 0)",
200
        $user->id, $now, $njobs, BATCH_STATE_INIT, $batch_name, $buda_boinc_app->id,
201
        $description
202
    ));
203
    return BoincBatch::lookup_id($batch_id);
204
}
205
206
function stage_input_files($batch_dir, $batch_desc, $batch_id) {
207
    $n = count($batch_desc->shared_files);
208
    $batch_desc->shared_files_phys_names = [];
209
    for ($i=0; $i<$n; $i++) {
210
        $path = sprintf('%s/%s', $batch_dir, $batch_desc->shared_files[$i]);
211
        [$md5, $size] = $batch_desc->shared_file_infos[$i];
212
        $phys_name = sprintf('batch_%d_%s', $batch_id, $md5);
213
        stage_file_aux($path, $md5, $size, $phys_name);
214
        $batch_desc->shared_files_phys_names[] = $phys_name;
215
    }
216
    foreach ($batch_desc->jobs as $job) {
217
        $n = count($batch_desc->unshared_files);
218
        $job->phys_names = [];
219
        for ($i=0; $i<$n; $i++) {
220
            $path = sprintf('%s/%s/%s',
221
                $batch_dir, $job->dir, $batch_desc->unshared_files[$i]
222
            );
223
            [$md5, $size] = $job->file_infos[$i];
224
            $phys_name = sprintf('batch_%d_%s', $batch_id, $md5);
225
            stage_file_aux($path, $md5, $size, $phys_name);
226
            $job->phys_names[] = $phys_name;
227
        }
228
    }
229
}
230
231
// run bin/create_work to create the jobs.
232
// Use --stdin, where each job is described by a line
233
//
234
function create_jobs(
235
    $user, $app, $app_desc, $batch_desc, $batch_id, $batch_dir_name,
236
    $wrapper_verbose, $cmdline, $max_fpops, $exp_fpops,
237
    $keywords
238
) {
239
    global $buda_boinc_app;
240
241
    // make per-job lines to pass as stdin
242
    //
243
    $job_cmds = '';
244
    foreach ($batch_desc->jobs as $job) {
245
        $job_cmd = sprintf('--wu_name batch_%d__job_%s', $batch_id, $job->dir);
246
        if ($job->cmdline) {
247
            $job_cmd .= sprintf(' --command_line "%s"', $job->cmdline);
248
        }
249
        foreach ($batch_desc->shared_files_phys_names as $x) {
250
            $job_cmd .= " $x";
251
        }
252
        foreach ($job->phys_names as $x) {
253
            $job_cmd .= " $x";
254
        }
255
        $job_cmds .= "$job_cmd\n";
256
    }
257
    $wrapper_cmdline = sprintf('"%s %s"',
258
        $wrapper_verbose?'--verbose':'',
259
        $cmdline
260
    );
261
    $cmd = sprintf(
262
        'cd ../..; bin/create_work --appname %s --sub_appname "%s" --batch %d --stdin --command_line %s --wu_template %s --result_template %s --rsc_fpops_bound %f --rsc_fpops_est %f',
263
        $buda_boinc_app->name,
264
        $app_desc->long_name,
265
        $batch_id,
266
        $wrapper_cmdline,
267
        "buda_apps/$app/template_in",
268
        "buda_apps/$app/template_out",
269
        $max_fpops, $exp_fpops
270
    );
271
    if ($keywords) {
272
        $cmd .= " --keywords '$keywords'";
273
    }
274
    if ($user->seti_id) {
275
        $cmd .= " --target_user $user->id ";
276
    }
277
    $cmd .= sprintf(' > %s 2<&1', "buda_batches/errfile");
278
279
    $h = popen($cmd, "w");
280
    if (!$h) error_page('create_work launch failed');
0 ignored issues
show
introduced by
$h is of type resource, thus it always evaluated to false.
Loading history...
281
    fwrite($h, $job_cmds);
282
    $ret = pclose($h);
283
    if ($ret) {
284
        echo "<pre>create_work failed.\n";
285
        echo "command: $cmd\n\n";
286
        echo "job lines:\n$job_cmds\n\n";
287
        echo "error file:\n";
288
        readfile("../../buda_batches/errfile");
289
        exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
290
    }
291
}
292
293
function handle_submit($user) {
294
    global $buda_root;
295
296
    $app = get_str('app');
297
    if (!is_valid_filename($app)) die('bad arg');
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
298
    $app_desc = get_buda_app_desc($app);
299
    if (!user_can_submit($user, $app_desc)) {
300
        error_page('no permission');
301
    }
302
303
    $batch_file = get_str('batch_file');
304
    if (!is_valid_filename($batch_file)) die('bad arg');
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
305
    $wrapper_verbose = get_str('wrapper_verbose', true);
306
    $cmdline = get_str('cmdline');
307
308
    $max_runtime_days = get_str('max_runtime_days');
309
    if (!is_numeric($max_runtime_days)) error_page('bad runtime limit');
310
    $max_runtime_days = (double)$max_runtime_days;
311
    if ($max_runtime_days <= 0) error_page('bad runtime limit');
312
    if ($max_runtime_days > 100) error_page('bad runtime limit');
313
314
    $max_fpops = $max_runtime_days * AVG_CPU_FPOPS * 86400;
315
316
    $exp_runtime_days = get_str('exp_runtime_days');
317
    if (!is_numeric($exp_runtime_days)) error_page('bad expected runtime');
318
    $exp_runtime_days = (double)$exp_runtime_days;
319
    if ($exp_runtime_days <= 0) error_page('bad expected runtime');
320
    if ($exp_runtime_days > 100) error_page('bad expected runtime');
321
    if ($exp_runtime_days > $max_runtime_days) {
322
        error_page('exp must be < max runtime');
323
    }
324
    $exp_fpops = $exp_runtime_days * AVG_CPU_FPOPS * 86400;
325
326
    // unzip batch file into temp dir
327
    $batch_dir_name = unzip_batch_file($user, $batch_file);
328
    $batch_dir = "../../buda_batches/$batch_dir_name";
329
330
    // scan batch dir; validate and return struct
331
    $batch_desc = parse_batch_dir($batch_dir, $app_desc);
332
333
    if (!$batch_desc->jobs) {
334
        system("rm -rf $batch_dir");
335
        page_head("No jobs created");
336
        echo "
337
            Your batch file (.zip) did not specify any jobs.
338
            See <a href=https://github.com/BOINC/boinc/wiki/BUDA-job-submission#batch-files>Instructions for creating batch files</a>.
339
        ";
340
        page_tail();
341
        return;
342
    }
343
    $njobs = count($batch_desc->jobs);
344
    if ($njobs > 10 && $user->seti_id) {
345
        system("rm -rf $batch_dir");
346
        error_page(
347
            "Batches with > 10 jobs are not allowed if 'use only my computers' is set"
348
        );
349
    }
350
    $us = BoincUserSubmit::lookup_userid($user->id);
351
    if ($us->max_jobs_in_progress) {
352
        $n = n_jobs_in_progress($user->id);
353
        if ($n + $njobs > $us->max_jobs_in_progress) {
354
            system("rm -rf $batch_dir");
355
            error_page(
356
                sprintf(
357
                    'This batch is %d jobs, and you already have %d in-progress jobs.
358
                    This would exceed your limit of %d in-progress jobs.
359
                    ',
360
                    $njobs, $n, $us->max_jobs_in_progress
361
                )
362
            );
363
        }
364
    }
365
366
    $batch = create_batch($user, count($batch_desc->jobs), $app);
367
368
    // stage input files and record the physical names
369
    //
370
    stage_input_files($batch_dir, $batch_desc, $batch->id);
371
372
    // get job keywords: user keywords plus BUDA app keywords
373
    //
374
    [$yes, $no] = read_kw_prefs($user);
375
    $keywords = array_merge($yes, $app_desc->sci_kw);
376
    $keywords = array_unique($keywords);
377
    $keywords = implode(' ', $keywords);
378
379
    create_jobs(
380
        $user, $app, $app_desc, $batch_desc, $batch->id, $batch_dir_name,
381
        $wrapper_verbose, $cmdline, $max_fpops, $exp_fpops, $keywords
382
    );
383
384
    // mark batch as in progress
385
    //
386
    $batch->update(sprintf('state=%d', BATCH_STATE_IN_PROGRESS));
387
388
    // clean up batch dir
389
    //
390
    system("rm -rf $batch_dir");
391
392
    header("Location: submit.php?action=query_batch&batch_id=$batch->id");
393
}
394
395
function show_app_list($user) {
396
    page_head('BUDA job submission');
397
    $apps = get_buda_apps();
398
    echo 'Select app:<p><br>';
399
    $found = false;
400
    foreach ($apps as $app) {
401
        $desc = get_buda_app_desc($app);
402
        if (!user_can_submit($user, $desc)) {
403
            continue;
404
        }
405
        $found = true;
406
        echo sprintf('<p><a href=buda_submit.php?action=form&app=%s>%s</a>',
407
            $app, $desc->long_name
408
        );
409
    }
410
    if (!$found) {
411
        echo "You don't have submit permissions for any BUDA apps.";
412
    }
413
    page_tail();
414
}
415
416
$user = get_logged_in_user();
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $user is correct as get_logged_in_user() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
417
$buda_boinc_app = BoincApp::lookup("name='buda'");
418
if (!$buda_boinc_app) error_page('no buda app');
419
if (!has_submit_access($user, $buda_boinc_app->id)) {
420
    error_page('no access');
421
}
422
$action = get_str('action', true);
423
if ($action == 'submit') {
424
    handle_submit($user);
425
} else if ($action == 'form') {
426
    submit_form($user);
427
} else {
428
    show_app_list($user);
429
}
430
431
?>
432