Completed
Pull Request — master (#51)
by Hiraku
02:26
created

ParallelDownloader::filterPackages()   C

Complexity

Conditions 7
Paths 18

Size

Total Lines 35
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 19
CRAP Score 7.6773

Importance

Changes 2
Bugs 1 Features 0
Metric Value
c 2
b 1
f 0
dl 0
loc 35
ccs 19
cts 25
cp 0.76
rs 6.7272
cc 7
eloc 25
nc 18
nop 2
crap 7.6773
1
<?php
2
/*
3
 * hirak/prestissimo
4
 * @author Hiraku NAKANO
5
 * @license MIT https://github.com/hirak/prestissimo
6
 */
7
namespace Hirak\Prestissimo;
8
9
use Composer\Package;
10
use Composer\IO;
11
use Composer\Config as CConfig;
12
13
/**
14
 *
15
 */
16
class ParallelDownloader
17
{
18
    /** @var IO/IOInterface */
19
    protected $io;
20
21
    /** @var CConfig */
22
    protected $config;
23
24
    /** @var int */
25
    protected $totalCnt = 0;
26
    protected $successCnt = 0;
27
    protected $skippedCnt = 0;
28
    protected $failureCnt = 0;
29
30 4
    public function __construct(IO\IOInterface $io, CConfig $config)
31
    {
32 4
        $this->io = $io;
33 4
        $this->config = $config;
34 4
    }
35
36
    /**
37
     * @param Package\PackageInterface[] $packages
38
     * @param array $pluginConfig
39
     * @return void
40
     */
41 3
    public function download(array $packages, array $pluginConfig)
42
    {
43 3
        $mh = curl_multi_init();
44 3
        $unused = array();
45 3
        for ($i = 0; $i < $pluginConfig['maxConnections']; ++$i) {
46 3
            $unused[] = curl_init();
47 3
        }
48
49 3
        $this->setupShareHandler($mh, $unused, $pluginConfig);
50
51 3
        $using = array(); //memory pool
52 3
        $running = $remains = 0;
53 3
        $this->totalCnt = count($packages);
54 3
        $this->successCnt = $this->skippedCnt = $this->failureCnt = 0;
55 3
        $this->io->write("    Prefetch start: <comment>total: $this->totalCnt</comment>");
56
57 3
        $targets = $this->filterPackages($packages, $pluginConfig);
58
        EVENTLOOP:
59
        // prepare curl resources
60 3
        while (count($unused) > 0 && count($targets) > 0) {
61 2
            $target = array_pop($targets);
62 2
            $ch = array_pop($unused);
63
64 2
            $using[(int)$ch] = $target;
65 2
            $onPreDownload = Factory::getPreEvent($target['src']);
66 2
            $onPreDownload->notify();
67
68 2
            $opts = $target['src']->getCurlOpts();
69
            // ParallelDownloader doesn't support private packages.
70 2
            unset($opts[CURLOPT_ENCODING], $opts[CURLOPT_USERPWD]);
71 2
            curl_setopt_array($ch, $opts);
72 2
            curl_setopt($ch, CURLOPT_FILE, $target['dest']->getPointer());
73 2
            curl_multi_add_handle($mh, $ch);
74 2
        }
75
76
        // wait for any event
77
        do {
78 3
            $runningBefore = $running;
79 3
            while (CURLM_CALL_MULTI_PERFORM === curl_multi_exec($mh, $running));
80
81
            SELECT:
82 3
            if (-1 === curl_multi_select($mh, 5)) {
83 2
                usleep(200 * 1000);
84 2
                continue;
85
            }
86
87 2
            while (CURLM_CALL_MULTI_PERFORM === curl_multi_exec($mh, $running));
88 2
            if ($running > 0 && $running === $runningBefore) {
89
                goto SELECT;
90
            }
91
92
            do {
93 2
                if ($raised = curl_multi_info_read($mh, $remains)) {
94 2
                    $ch = $raised['handle'];
95 2
                    $errno = curl_errno($ch);
96 2
                    $info = curl_getinfo($ch);
97 2
                    curl_setopt($ch, CURLOPT_FILE, STDOUT);
98 2
                    $index = (int)$ch;
99 2
                    $target = $using[$index];
100 2
                    unset($using[$index]);
101 2
                    if (CURLE_OK === $errno && 200 === $info['http_code']) {
102 1
                        ++$this->successCnt;
103 1
                        $target['dest']->setSuccess();
104 1
                    } else {
105 1
                        ++$this->failureCnt;
106
                    }
107 2
                    unset($target);
108 2
                    $this->io->write($this->makeDownloadingText($info['url']));
109 2
                    curl_multi_remove_handle($mh, $ch);
110 2
                    $unused[] = $ch;
111 2
                }
112 2
            } while ($remains > 0);
113
114 2
            if (count($targets) > 0) {
115
                goto EVENTLOOP;
116
            }
117 3
        } while ($running > 0);
118
119 3
        $this->io->write("    Finished: <comment>success: $this->successCnt, skipped: $this->skippedCnt, failure: $this->failureCnt, total: $this->totalCnt</comment>");
120
121 3
        foreach ($unused as $ch) {
122 3
            curl_close($ch);
123 3
        }
124 3
        curl_multi_close($mh);
125 3
    }
126
127
    /**
128
     * @codeCoverageIgnore
129
     */
130
    private function setupShareHandler($mh, array $unused, array $pluginConfig)
131
    {
132
        if (function_exists('curl_share_init')) {
133
            $sh = curl_share_init();
134
            curl_share_setopt($sh, CURLSHOPT_SHARE, CURL_LOCK_DATA_SSL_SESSION);
135
136
            foreach ($unused as $ch) {
137
                curl_setopt($ch, CURLOPT_SHARE, $sh);
138
            }
139
        }
140
141
        if (function_exists('curl_multi_setopt')) {
142
            if ($pluginConfig['pipeline']) {
143
                curl_multi_setopt($mh, CURLMOPT_PIPELINING, true);
144
            }
145
        }
146
    }
147
148
    /**
149
     * @param Package\PackageInterface[] $packages
150
     * @param string[] $pluginConfig
151
     * @return [{src: Aspects\HttpGetRequest, dest: OutputFile}]
0 ignored issues
show
Documentation introduced by
The doc-type {src:">[{src: could not be parsed: Unknown type name "[" at position 0. [(view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
152
     */
153 3
    private function filterPackages(array $packages, array $pluginConfig)
154
    {
155 3
        $cachedir = rtrim($this->config->get('cache-files-dir'), '\/');
156 3
        $zips = array();
157 3
        foreach ($packages as $p) {
158 3
            $url = $p->getDistUrl();
159 3
            if (!$url) {
160 1
                ++$this->skippedCnt;
161 1
                continue;
162
            }
163 2
            if ($p->getDistMirrors()) {
164
                $url = current($p->getDistUrls());
165
            }
166 2
            $host = parse_url($url, PHP_URL_HOST) ?: '';
167 2
            $src = Factory::getHttpGetRequest($host, $url, $this->io, $this->config, $pluginConfig);
168 2
            if (in_array($p->getName(), $pluginConfig['privatePackages'])) {
169
                $src->maybePublic = false;
170
            } else {
171 2
                $src->maybePublic = (bool)preg_match('%^(?:https|git)://github\.com%', $p->getSourceUrl());
172
            }
173
            // make file resource
174
            $filepath = $cachedir
175
                . DIRECTORY_SEPARATOR
176 2
                . FileDownloaderDummy::getCacheKeyCompat($p, $src->getURL());
177 2
            if (file_exists($filepath)) {
178
                ++$this->skippedCnt;
179
                continue;
180
            }
181 2
            $dest = new OutputFile($filepath);
182
183 2
            $zips[] = compact('src', 'dest');
184 3
        }
185
186 3
        return $zips;
187
    }
188
189
    /**
190
     * @param string $url
191
     * @return string
192
     */
193 2
    private function makeDownloadingText($url)
194
    {
195 2
        $request = new Aspects\HttpGetRequest('example.com', $url, $this->io);
196 2
        $request->query = array();
197 2
        return "    <comment>$this->successCnt/$this->totalCnt</comment>:    {$request->getURL()}";
198
    }
199
}
200