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

ParallelDownloader::setupShareHandler()   B

Complexity

Conditions 5
Paths 6

Size

Total Lines 17
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 17
ccs 0
cts 0
cp 0
rs 8.8571
cc 5
eloc 9
nc 6
nop 3
crap 30
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 3
    public function __construct(IO\IOInterface $io, CConfig $config)
31
    {
32 3
        $this->io = $io;
33 3
        $this->config = $config;
34 3
    }
35
36
    /**
37
     * @param Package\PackageInterface[] $packages
38
     * @param array $pluginConfig
39
     * @return void
40
     */
41 2
    public function download(array $packages, array $pluginConfig)
42
    {
43 2
        $mh = curl_multi_init();
44 2
        $unused = array();
45 2
        $maxConns = $pluginConfig['maxConnections'];
46 2
        for ($i = 0; $i < $maxConns; ++$i) {
47 2
            $unused[] = curl_init();
48 2
        }
49
50 2
        $this->setupShareHandler($mh, $unused, $pluginConfig);
51
52 2
        $chFpMap = array();
53 2
        $running = 0; //ref type
54 2
        $remains = 0; //ref type
55
56 2
        $this->totalCnt = count($packages);
57 2
        $this->successCnt = 0;
58 2
        $this->skippedCnt = 0;
59 2
        $this->failureCnt = 0;
60 2
        $this->io->write("    Prefetch start: total: $this->totalCnt</comment>");
61
62 2
        $cachedir = rtrim($this->config->get('cache-files-dir'), '\/');
63
64
        EVENTLOOP:
65
        // prepare curl resources
66 2
        while (count($unused) > 0 && count($packages) > 0) {
67 2
            $package = array_pop($packages);
68 2
            $filepath = $cachedir . DIRECTORY_SEPARATOR . static::getCacheKey($package);
69 2
            if (file_exists($filepath)) {
70
                ++$this->skippedCnt;
71
                continue;
72
            }
73 2
            $ch = array_pop($unused);
74
75
            // make file resource
76 2
            $chFpMap[(int)$ch] = $outputFile = new OutputFile($filepath);
77
78
            // make url
79 2
            $url = $package->getDistUrl();
80 2
            if (! $url) {
81 1
                ++$this->skippedCnt;
82 1
                continue;
83
            }
84 1
            if ($package->getDistMirrors()) {
85
                $url = current($package->getDistUrls());
86
            }
87 1
            $host = parse_url($url, PHP_URL_HOST) ?: '';
88 1
            $request = Factory::getHttpGetRequest($host, $url, $this->io, $this->config, $pluginConfig);
89 1
            if (in_array($package->getName(), $pluginConfig['privatePackages'])) {
90
                $request->maybePublic = false;
91
            } else {
92 1
                $request->maybePublic = (bool)preg_match('%^(?:https|git)://github\.com%', $package->getSourceUrl());
93
            }
94 1
            $onPreDownload = Factory::getPreEvent($request);
95 1
            $onPreDownload->notify();
96
97 1
            $opts = $request->getCurlOpts();
98 1
            unset($opts[CURLOPT_ENCODING]);
99 1
            unset($opts[CURLOPT_USERPWD]); // ParallelDownloader doesn't support private packages.
100 1
            curl_setopt_array($ch, $opts);
101 1
            curl_setopt($ch, CURLOPT_FILE, $outputFile->getPointer());
102 1
            curl_multi_add_handle($mh, $ch);
103 1
        }
104
105
        // wait for any event
106
        do {
107 2
            $runningBefore = $running;
108 2
            while (CURLM_CALL_MULTI_PERFORM === curl_multi_exec($mh, $running)) {
0 ignored issues
show
Unused Code introduced by
This while loop is empty and can be removed.

This check looks for while loops that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

Consider removing the loop.

Loading history...
109
110
            }
111
112
            SELECT:
113 2
            $eventCount = curl_multi_select($mh, 5);
114
115 2
            if ($eventCount === -1) {
116 1
                usleep(200 * 1000);
117 1
                continue;
118
            }
119
120 1
            while (CURLM_CALL_MULTI_PERFORM === curl_multi_exec($mh, $running)) {
0 ignored issues
show
Unused Code introduced by
This while loop is empty and can be removed.

This check looks for while loops that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

Consider removing the loop.

Loading history...
121
122
            }
123
124 1
            if ($running > 0 && $running === $runningBefore) {
125
                goto SELECT;
126
            }
127
128
            do {
129 1
                if ($raised = curl_multi_info_read($mh, $remains)) {
130 1
                    $ch = $raised['handle'];
131 1
                    $errno = curl_errno($ch);
132 1
                    $info = curl_getinfo($ch);
133 1
                    curl_setopt($ch, CURLOPT_FILE, STDOUT);
134 1
                    $index = (int)$ch;
135 1
                    $outputFile = $chFpMap[$index];
136 1
                    unset($chFpMap[$index]);
137 1
                    if (CURLE_OK === $errno && 200 === $info['http_code']) {
138
                        ++$this->successCnt;
139
                        $outputFile->setSuccess();
140
                    } else {
141 1
                        ++$this->failureCnt;
142
                    }
143 1
                    unset($outputFile);
144 1
                    $this->io->write($this->makeDownloadingText($info['url']));
145 1
                    curl_multi_remove_handle($mh, $ch);
146 1
                    $unused[] = $ch;
147 1
                }
148 1
            } while ($remains > 0);
149
150 1
            if (count($packages) > 0) {
151
                goto EVENTLOOP;
152
            }
153 2
        } while ($running > 0);
154
155 2
        $this->io->write("    Finished: <comment>success: $this->successCnt, skipped: $this->skippedCnt, failure: $this->failureCnt, total: $this->totalCnt</comment>");
156
157 2
        foreach ($unused as $ch) {
158 2
            curl_close($ch);
159 2
        }
160 2
        curl_multi_close($mh);
161 2
    }
162
163
    /**
164
     * @codeCoverageIgnore
165
     */
166
    private function setupShareHandler($mh, array $unused, array $pluginConfig)
167
    {
168
        if (function_exists('curl_share_init')) {
169
            $sh = curl_share_init();
170
            curl_share_setopt($sh, CURLSHOPT_SHARE, CURL_LOCK_DATA_SSL_SESSION);
171
172
            foreach ($unused as $ch) {
173
                curl_setopt($ch, CURLOPT_SHARE, $sh);
174
            }
175
        }
176
177
        if (function_exists('curl_multi_setopt')) {
178
            if ($pluginConfig['pipeline']) {
179
                curl_multi_setopt($mh, CURLMOPT_PIPELINING, true);
180
            }
181
        }
182
    }
183
184
    /**
185
     * @param string $url
186
     * @return string
187
     */
188 1
    private function makeDownloadingText($url)
189
    {
190 1
        $request = new Aspects\HttpGetRequest('example.com', $url, $this->io);
191 1
        $request->query = array();
192 1
        return "    <comment>$this->successCnt/$this->totalCnt</comment>:    {$request->getURL()}";
193
    }
194
195 2
    public static function getCacheKey(Package\PackageInterface $p)
196
    {
197 2
        $distRef = $p->getDistReference();
198 2
        if (preg_match('{^[a-f0-9]{40}$}', $distRef)) {
199
            return "{$p->getName()}/$distRef.{$p->getDistType()}";
200
        }
201
202 2
        return "{$p->getName()}/{$p->getVersion()}-$distRef.{$p->getDistType()}";
203
    }
204
}
205