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

98
    /** @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...
99
    for ($i=0; $i<1000; $i++) {
100
        $batch_dir = "../../buda_batches/$i";
101
        $batch_dir_name = $i;
102
        $ret = @mkdir($batch_dir);
103
        if ($ret) break;
104
    }
105
    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...
106
    $sb_dir = sandbox_dir($user);
107
    if (!file_exists("$sb_dir/$batch_file")) {
108
        error_page("no batch file $batch_file");
109
    }
110
    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...
111
    if ($ret) {
112
        error_page("unzip error: $ret");
113
    }
114
    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...
115
}
116
117
// Scan a batch dir.
118
// Check its validity:
119
// - optional dir 'shared_input_files' has shared input files
120
// - other dirs (job dirs) can have only remaining infiles and possibly cmdline
121
//
122
// Return a structure describing its contents, and the md5/size of files
123
//
124
function parse_batch_dir($batch_dir, $app_desc) {
125
    $input_files = $app_desc->input_file_names;
126
    sort($input_files);
127
    $shared_files = [];
128
    $shared_file_infos = [];
129
    if (is_dir("$batch_dir/shared_input_files")) {
130
        foreach (scandir("$batch_dir/shared_input_files") as $fname) {
131
            if ($fname[0] == '.') continue;
132
            if (!in_array($fname, $input_files)) {
133
                error_page("$fname is not an input file name");
134
            }
135
            $shared_files[] = $fname;
136
            $shared_file_infos[] = get_file_info("$batch_dir/shared_input_files/$fname");
137
        }
138
    }
139
    $unshared_files = array_diff($input_files, $shared_files);
140
    sort($unshared_files);
141
    $jobs = [];
142
    foreach (scandir($batch_dir) as $fname) {
143
        if ($fname[0] == '.') continue;
144
        if ($fname == 'shared_input_files') continue;
145
        if (!is_dir("$batch_dir/$fname")) {
146
            error_page("$batch_dir/$fname is not a directory");
147
        }
148
        $job_files = [];
149
        $cmdline = '';
150
        foreach(scandir("$batch_dir/$fname") as $f2) {
151
            if ($f2[0] == '.') continue;
152
            if ($f2 == 'cmdline') {
153
                $cmdline = trim(file_get_contents("$batch_dir/$fname/cmdline"));
154
                continue;
155
            }
156
            if (!in_array($f2, $unshared_files)) {
157
                error_page("$fname/$f2 is not an input file name");
158
            }
159
            $job_files[] = $f2;
160
        }
161
        if (array_values($job_files) != array_values($unshared_files)) {
162
            error_page("$fname doesn't have all input files");
163
        }
164
165
        if (!$cmdline && !$job_files) {
166
            error_page("job $f2 has no cmdline and no input files");
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $f2 does not seem to be defined for all execution paths leading up to this point.
Loading history...
167
        }
168
169
        $file_infos = [];
170
        foreach ($unshared_files as $f2) {
171
            $file_infos[] = get_file_info("$batch_dir/$fname/$f2");
172
        }
173
174
        $job = new StdClass;
175
        $job->dir = $fname;
176
        $job->cmdline = $cmdline;
177
        $job->file_infos = $file_infos;
178
        $jobs[] = $job;
179
    }
180
    $batch_desc = new StdClass;
181
    $batch_desc->shared_files = $shared_files;
182
    $batch_desc->shared_file_infos = $shared_file_infos;
183
    $batch_desc->unshared_files = $unshared_files;
184
    $batch_desc->jobs = $jobs;
185
    return $batch_desc;
186
}
187
188
function create_batch($user, $njobs, $app) {
189
    global $buda_boinc_app;
190
    $now = time();
191
    $batch_name = sprintf('buda_%d_%d', $user->id, $now);
192
    $description = "$app";
193
    $batch_id = BoincBatch::insert(sprintf(
194
        "(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)",
195
        $user->id, $now, $njobs, BATCH_STATE_INIT, $batch_name, $buda_boinc_app->id,
196
        $description
197
    ));
198
    return BoincBatch::lookup_id($batch_id);
199
}
200
201
function stage_input_files($batch_dir, $batch_desc, $batch_id) {
202
    $n = count($batch_desc->shared_files);
203
    $batch_desc->shared_files_phys_names = [];
204
    for ($i=0; $i<$n; $i++) {
205
        $path = sprintf('%s/%s', $batch_dir, $batch_desc->shared_files[$i]);
206
        [$md5, $size] = $batch_desc->shared_file_infos[$i];
207
        $phys_name = sprintf('batch_%d_%s', $batch_id, $md5);
208
        stage_file_aux($path, $md5, $size, $phys_name);
209
        $batch_desc->shared_files_phys_names[] = $phys_name;
210
    }
211
    foreach ($batch_desc->jobs as $job) {
212
        $n = count($batch_desc->unshared_files);
213
        $job->phys_names = [];
214
        for ($i=0; $i<$n; $i++) {
215
            $path = sprintf('%s/%s/%s',
216
                $batch_dir, $job->dir, $batch_desc->unshared_files[$i]
217
            );
218
            [$md5, $size] = $job->file_infos[$i];
219
            $phys_name = sprintf('batch_%d_%s', $batch_id, $md5);
220
            stage_file_aux($path, $md5, $size, $phys_name);
221
            $job->phys_names[] = $phys_name;
222
        }
223
    }
224
}
225
226
// run bin/create_work to create the jobs.
227
// Use --stdin, where each job is described by a line
228
//
229
function create_jobs(
230
    $user, $app, $app_desc, $batch_desc, $batch_id, $batch_dir_name,
0 ignored issues
show
Coding Style introduced by
Multi-line function declarations must define one parameter per line
Loading history...
231
    $wrapper_verbose, $cmdline, $max_fpops, $exp_fpops,
0 ignored issues
show
Coding Style introduced by
Multi-line function declarations must define one parameter per line
Loading history...
232
    $keywords
233
) {
234
    global $buda_boinc_app;
235
236
    // make per-job lines to pass as stdin
237
    //
238
    $job_cmds = '';
239
    foreach ($batch_desc->jobs as $job) {
240
        $job_cmd = sprintf('--wu_name batch_%d__job_%s', $batch_id, $job->dir);
241
        if ($job->cmdline) {
242
            $job_cmd .= sprintf(' --command_line "%s"', $job->cmdline);
243
        }
244
        foreach ($batch_desc->shared_files_phys_names as $x) {
245
            $job_cmd .= " $x";
246
        }
247
        foreach ($job->phys_names as $x) {
248
            $job_cmd .= " $x";
249
        }
250
        $job_cmds .= "$job_cmd\n";
251
    }
252
    $wrapper_cmdline = sprintf('"%s %s"',
253
        $wrapper_verbose?'--verbose':'',
254
        $cmdline
255
    );
256
    $cmd = sprintf(
257
        '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',
258
        $buda_boinc_app->name,
259
        $app_desc->long_name,
260
        $batch_id,
261
        $wrapper_cmdline,
262
        "buda_apps/$app/template_in",
263
        "buda_apps/$app/template_out",
264
        $max_fpops, $exp_fpops
265
    );
266
    if ($keywords) {
267
        $cmd .= " --keywords '$keywords'";
268
    }
269
    if ($user->seti_id) {
270
        $cmd .= " --target_user $user->id ";
271
    }
272
    $cmd .= sprintf(' > %s 2<&1', "buda_batches/errfile");
273
274
    $h = popen($cmd, "w");
275
    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...
276
    fwrite($h, $job_cmds);
277
    $ret = pclose($h);
278
    if ($ret) {
279
        echo "<pre>create_work failed.\n";
280
        echo "command: $cmd\n\n";
281
        echo "job lines:\n$job_cmds\n\n";
282
        echo "error file:\n";
283
        readfile("../../buda_batches/errfile");
284
        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...
285
    }
286
}
287
288
function handle_submit($user) {
289
    global $buda_root;
290
291
    $app = get_str('app');
292
    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...
293
    $batch_file = get_str('batch_file');
294
    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...
295
    $wrapper_verbose = get_str('wrapper_verbose', true);
296
    $cmdline = get_str('cmdline');
297
298
    $max_runtime_days = get_str('max_runtime_days');
299
    if (!is_numeric($max_runtime_days)) error_page('bad runtime limit');
300
    $max_runtime_days = (double)$max_runtime_days;
301
    if ($max_runtime_days <= 0) error_page('bad runtime limit');
302
    if ($max_runtime_days > 100) error_page('bad runtime limit');
303
304
    $max_fpops = $max_runtime_days * AVG_CPU_FPOPS * 86400;
305
306
    $exp_runtime_days = get_str('exp_runtime_days');
307
    if (!is_numeric($exp_runtime_days)) error_page('bad expected runtime');
308
    $exp_runtime_days = (double)$exp_runtime_days;
309
    if ($exp_runtime_days <= 0) error_page('bad expected runtime');
310
    if ($exp_runtime_days > 100) error_page('bad expected runtime');
311
    if ($exp_runtime_days > $max_runtime_days) {
312
        error_page('exp must be < max runtime');
313
    }
314
    $exp_fpops = $exp_runtime_days * AVG_CPU_FPOPS * 86400;
315
316
    $app_desc = get_buda_app_desc($app);
317
318
    // unzip batch file into temp dir
319
    $batch_dir_name = unzip_batch_file($user, $batch_file);
320
    $batch_dir = "../../buda_batches/$batch_dir_name";
321
322
    // scan batch dir; validate and return struct
323
    $batch_desc = parse_batch_dir($batch_dir, $app_desc);
324
325
    if (!$batch_desc->jobs) {
326
        system("rm -rf $batch_dir");
327
        page_head("No jobs created");
328
        echo "
329
            Your batch file (.zip) did not specify any jobs.
330
            See <a href=https://github.com/BOINC/boinc/wiki/BUDA-job-submission#batch-files>Instructions for creating batch files</a>.
331
        ";
332
        page_tail();
333
        return;
334
    }
335
    $njobs = count($batch_desc->jobs);
336
    if ($njobs > 10 && $user->seti_id) {
337
        system("rm -rf $batch_dir");
338
        error_page(
339
            "Batches with > 10 jobs are not allowed if 'use only my computers' is set"
340
        );
341
    }
342
    $us = BoincUserSubmit::lookup_userid($user->id);
343
    if ($us->max_jobs_in_progress) {
344
        $n = n_jobs_in_progress($user->id);
345
        if ($n + $njobs > $us->max_jobs_in_progress) {
346
            system("rm -rf $batch_dir");
347
            error_page(
348
                sprintf(
349
                    'This batch is %d jobs, and you already have %d in-progress jobs.
350
                    This would exceed your limit of %d in-progress jobs.
351
                    ',
352
                    $njobs, $n, $us->max_jobs_in_progress
353
                )
354
            );
355
        }
356
    }
357
358
    $batch = create_batch($user, count($batch_desc->jobs), $app);
359
360
    // stage input files and record the physical names
361
    //
362
    stage_input_files($batch_dir, $batch_desc, $batch->id);
363
364
    // get job keywords: user keywords plus BUDA app keywords
365
    //
366
    [$yes, $no] = read_kw_prefs($user);
367
    $keywords = array_merge($yes, $app_desc->sci_kw);
368
    $keywords = array_unique($keywords);
369
    $keywords = implode(' ', $keywords);
370
371
    create_jobs(
372
        $user, $app, $app_desc, $batch_desc, $batch->id, $batch_dir_name,
373
        $wrapper_verbose, $cmdline, $max_fpops, $exp_fpops, $keywords
374
    );
375
376
    // mark batch as in progress
377
    //
378
    $batch->update(sprintf('state=%d', BATCH_STATE_IN_PROGRESS));
379
380
    // clean up batch dir
381
    //
382
    system("rm -rf $batch_dir");
383
384
    header("Location: submit.php?action=query_batch&batch_id=$batch->id");
385
}
386
387
function show_list() {
388
    page_head('BUDA job submission');
389
    $apps = get_buda_apps();
390
    echo 'Select app:<p><br>';
391
    foreach ($apps as $app) {
392
        $desc = get_buda_app_desc($app);
393
        echo sprintf('<p><a href=buda_submit.php?action=form&app=%s>%s</a>',
394
            $app, $desc->long_name
395
        );
396
    }
397
    page_tail();
398
}
399
400
$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...
401
$buda_boinc_app = BoincApp::lookup("name='buda'");
402
if (!$buda_boinc_app) error_page('no buda app');
403
if (!has_submit_access($user, $buda_boinc_app->id)) {
404
    error_page('no access');
405
}
406
$action = get_str('action', true);
407
if ($action == 'submit') {
408
    handle_submit($user);
409
} else if ($action == 'form') {
410
    submit_form($user);
411
} else {
412
    show_list();
0 ignored issues
show
Bug introduced by
The call to show_list() has too few arguments starting with models. ( Ignorable by Annotation )

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

412
    /** @scrutinizer ignore-call */ 
413
    show_list();

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
413
}
414
415
?>
416