Completed
Push — master ( 410881...976b6a )
by Hiraku
02:34
created

ParallelDownloader   A

Complexity

Total Complexity 31

Size/Duplication

Total Lines 184
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 7

Test Coverage

Coverage 3.96%

Importance

Changes 18
Bugs 6 Features 1
Metric Value
wmc 31
c 18
b 6
f 1
lcom 1
cbo 7
dl 0
loc 184
ccs 4
cts 101
cp 0.0396
rs 9.8

4 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 5 1
F download() 0 138 27
A makeDownloadingText() 0 6 1
A getCacheKey() 0 9 2
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;
12
13
/**
14
 *
15
 */
16
class ParallelDownloader
17
{
18
    /** @var IO/IOInterface */
19
    protected $io;
20
21
    /** @var Config */
22
    protected $config;
23
24
    /** @var int */
25
    protected $totalCnt = 0;
26
    protected $successCnt = 0;
27
    protected $failureCnt = 0;
28
29 1
    public function __construct(IO\IOInterface $io, Config $config)
30
    {
31 1
        $this->io = $io;
32 1
        $this->config = $config;
33 1
    }
34
35
    /**
36
     * @param Package\PackageInterface[] $packages
37
     * @param array $pluginConfig
38
     * @return void
39
     */
40
    public function download(array $packages, array $pluginConfig)
41
    {
42
        $mh = curl_multi_init();
43
        $unused = array();
44
        $maxConns = $pluginConfig['maxConnections'];
45
        for ($i = 0; $i < $maxConns; ++$i) {
46
            $unused[] = curl_init();
47
        }
48
49
        // @codeCoverageIgnoreStart
50
        if (function_exists('curl_share_init')) {
51
            $sh = curl_share_init();
52
            curl_share_setopt($sh, CURLSHOPT_SHARE, CURL_LOCK_DATA_SSL_SESSION);
53
54
            foreach ($unused as $ch) {
55
                curl_setopt($ch, CURLOPT_SHARE, $sh);
56
            }
57
        }
58
59
        if (function_exists('curl_multi_setopt')) {
60
            if ($pluginConfig['pipeline']) {
61
                curl_multi_setopt($mh, CURLMOPT_PIPELINING, true);
62
            }
63
        }
64
        // @codeCoverageIgnoreEnd
65
66
        $cachedir = rtrim($this->config->get('cache-files-dir'), '\/');
67
68
        $chFpMap = array();
69
        $running = 0; //ref type
70
        $remains = 0; //ref type
71
72
        $this->totalCnt = count($packages);
73
        $this->successCnt = 0;
74
        $this->failureCnt = 0;
75
        $this->io->write("    Prefetch start: <comment>success: $this->successCnt, failure: $this->failureCnt, total: $this->totalCnt</comment>");
76
77
        EVENTLOOP:
78
        // prepare curl resources
79
        while (count($unused) > 0 && count($packages) > 0) {
80
            $package = array_pop($packages);
81
            $filepath = $cachedir . DIRECTORY_SEPARATOR . static::getCacheKey($package);
82
            if (file_exists($filepath)) {
83
                ++$this->successCnt;
84
                continue;
85
            }
86
            $ch = array_pop($unused);
87
88
            // make file resource
89
            $chFpMap[(int)$ch] = $outputFile = new OutputFile($filepath);
90
91
            // make url
92
            $url = $package->getDistUrl();
93
            $host = parse_url($url, PHP_URL_HOST) ?: '';
94
            $request = new Aspects\HttpGetRequest($host, $url, $this->io);
95
            $request->verbose = $pluginConfig['verbose'];
96
            if (in_array($package->getName(), $pluginConfig['privatePackages'])) {
97
                $request->maybePublic = false;
98
            } else {
99
                $request->maybePublic = (bool)preg_match('%^(?:https|git)://github\.com%', $package->getSourceUrl());
100
            }
101
            $onPreDownload = Factory::getPreEvent($request);
102
            $onPreDownload->notify();
103
104
            $opts = $request->getCurlOpts();
105
            if ($pluginConfig['insecure']) {
106
                $opts[CURLOPT_SSL_VERIFYPEER] = false;
107
            }
108
            if (! empty($pluginConfig['userAgent'])) {
109
                $opts[CURLOPT_USERAGENT] = $pluginConfig['userAgent'];
110
            }
111
            if (! empty($pluginConfig['capath'])) {
112
                $opts[CURLOPT_CAPATH] = $pluginConfig['capath'];
113
            }
114
            unset($opts[CURLOPT_ENCODING]);
115
            unset($opts[CURLOPT_USERPWD]); // ParallelDownloader doesn't support private packages.
116
            curl_setopt_array($ch, $opts);
117
            curl_setopt($ch, CURLOPT_FILE, $outputFile->getPointer());
118
            curl_multi_add_handle($mh, $ch);
119
        }
120
121
        // wait for any event
122
        do {
123
            $runningBefore = $running;
124
            while (CURLM_CALL_MULTI_PERFORM === curl_multi_exec($mh, $running));
125
126
            SELECT:
127
            $eventCount = curl_multi_select($mh, 5);
128
129
            if ($eventCount === -1) {
130
                usleep(200 * 1000);
131
                continue;
132
            }
133
134
            if ($eventCount === 0) {
135
                continue;
136
            }
137
138
            while (CURLM_CALL_MULTI_PERFORM === curl_multi_exec($mh, $running));
139
140
            if ($running > 0 && $running === $runningBefore) {
141
                goto SELECT;
142
            }
143
144
            do {
145
                if ($raised = curl_multi_info_read($mh, $remains)) {
146
                    $ch = $raised['handle'];
147
                    $errno = curl_errno($ch);
148
                    $info = curl_getinfo($ch);
149
                    curl_setopt($ch, CURLOPT_FILE, STDOUT);
150
                    $index = (int)$ch;
151
                    $outputFile = $chFpMap[$index];
152
                    unset($chFpMap[$index]);
153
                    if (CURLE_OK === $errno && 200 === $info['http_code']) {
154
                        ++$this->successCnt;
155
                    } else {
156
                        ++$this->failureCnt;
157
                        $outputFile->setFailure();
158
                    }
159
                    unset($outputFile);
160
                    $this->io->write($this->makeDownloadingText($info['url']));
161
                    curl_multi_remove_handle($mh, $ch);
162
                    $unused[] = $ch;
163
                }
164
            } while ($remains > 0);
165
166
            if (count($packages) > 0) {
167
                goto EVENTLOOP;
168
            }
169
        } while ($running > 0);
170
171
        $this->io->write("    Finished: <comment>success: $this->successCnt, failure: $this->failureCnt, total: $this->totalCnt</comment>");
172
173
        foreach ($unused as $ch) {
174
            curl_close($ch);
175
        }
176
        curl_multi_close($mh);
177
    }
178
179
    /**
180
     * @param string $url
181
     * @return string
182
     */
183
    private function makeDownloadingText($url)
184
    {
185
        $request = new Aspects\HttpGetRequest('example.com', $url, $this->io);
186
        $request->query = array();
187
        return "    <comment>$this->successCnt/$this->totalCnt</comment>:    {$request->getURL()}";
188
    }
189
190
    public static function getCacheKey(Package\PackageInterface $p)
191
    {
192
        $distRef = $p->getDistReference();
193
        if (preg_match('{^[a-f0-9]{40}$}', $distRef)) {
194
            return "{$p->getName()}/$distRef.{$p->getDistType()}";
195
        }
196
197
        return "{$p->getName()}/{$p->getVersion()}-$distRef.{$p->getDistType()}";
198
    }
199
}
200