├── .github
├── FUNDING.yml
└── ISSUE_TEMPLATE
│ └── bug_report.md
├── .gitignore
├── .scrutinizer.yml
├── .travis.yml
├── KnownIssues-CN.md
├── KnownIssues.md
├── LICENSE
├── README-CN.md
├── README.md
├── Settings-CN.md
├── Settings.md
├── bin
├── fswatch
├── inotify
└── laravels
├── composer.json
├── config
├── laravels.php
└── prometheus.php
├── grafana-dashboard.json
├── grafana-dashboard.png
├── logo.svg
├── phpunit.xml
├── sponsor.png
├── src
├── Components
│ ├── Apollo
│ │ ├── Client.php
│ │ └── Process.php
│ ├── HttpClient
│ │ └── SimpleHttpTrait.php
│ ├── MetricCollector.php
│ ├── MetricCollectorInterface.php
│ └── Prometheus
│ │ ├── CollectorProcess.php
│ │ ├── Collectors
│ │ ├── HttpRequestCollector.php
│ │ ├── SwooleProcessCollector.php
│ │ ├── SwooleStatsCollector.php
│ │ └── SystemCollector.php
│ │ ├── Exporter.php
│ │ ├── RequestMiddleware.php
│ │ ├── ServiceProvider.php
│ │ └── TimerProcessMetricsCronJob.php
├── Console
│ └── Portal.php
├── Illuminate
│ ├── CleanerManager.php
│ ├── Cleaners
│ │ ├── AuthCleaner.php
│ │ ├── BaseCleaner.php
│ │ ├── CleanerInterface.php
│ │ ├── ConfigCleaner.php
│ │ ├── ContainerCleaner.php
│ │ ├── CookieCleaner.php
│ │ ├── DcatAdminCleaner.php
│ │ ├── JWTCleaner.php
│ │ ├── LaravelAdminCleaner.php
│ │ ├── MenuCleaner.php
│ │ ├── RequestCleaner.php
│ │ ├── SessionCleaner.php
│ │ └── ZiggyCleaner.php
│ ├── Laravel.php
│ ├── LaravelSCommand.php
│ ├── LaravelSServiceProvider.php
│ ├── LaravelScheduleJob.php
│ ├── LaravelTrait.php
│ ├── ListPropertiesCommand.php
│ ├── LogTrait.php
│ └── ReflectionApp.php
├── LaravelS.php
└── Swoole
│ ├── Coroutine
│ └── Context.php
│ ├── DynamicResponse.php
│ ├── Events
│ ├── ServerStartInterface.php
│ ├── ServerStopInterface.php
│ ├── WorkerErrorInterface.php
│ ├── WorkerStartInterface.php
│ └── WorkerStopInterface.php
│ ├── Inotify.php
│ ├── InotifyTrait.php
│ ├── Process
│ ├── CustomProcessInterface.php
│ ├── CustomProcessTrait.php
│ └── ProcessTitleTrait.php
│ ├── Request.php
│ ├── Response.php
│ ├── ResponseInterface.php
│ ├── Server.php
│ ├── Socket
│ ├── Http.php
│ ├── HttpInterface.php
│ ├── PortInterface.php
│ ├── TcpInterface.php
│ ├── TcpSocket.php
│ ├── UdpInterface.php
│ ├── UdpSocket.php
│ ├── WebSocket.php
│ └── WebSocketInterface.php
│ ├── StaticResponse.php
│ ├── Task
│ ├── BaseTask.php
│ ├── Event.php
│ ├── Listener.php
│ └── Task.php
│ ├── Timer
│ ├── BackupCronJob.php
│ ├── CheckGlobalTimerAliveCronJob.php
│ ├── CronJob.php
│ ├── CronJobInterface.php
│ ├── RenewGlobalTimerLockCronJob.php
│ └── TimerTrait.php
│ └── WebSocketHandlerInterface.php
└── tests
├── SimpleHttpTest.php
├── TestCase.php
└── avg-qps.sh
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
4 | patreon: # Replace with a single Patreon username
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | otechie: # Replace with a single Otechie username
12 | #custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
13 |
14 | custom: ['https://www.paypal.me/hhxsv5','https://www.blockchain.com/btc/address/367HnAzVTAEk8offesDhcsCQswnugsE54u','https://gitee.com/hhxsv5/laravel-s?donate=true']
15 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: analyzing
6 | assignees: hhxsv5
7 |
8 | ---
9 |
10 | 1. Your software version (Screenshot of your startup)
11 |
12 | | Software | Version |
13 | | --------- | --------- |
14 | | PHP | TODO |
15 | | Swoole | TODO |
16 | | Laravel/Lumen | TODO |
17 |
18 | 2. Detail description about this issue(error/log)
19 |
20 | `TODO`
21 |
22 | 3. Some `reproducible` code blocks and `steps`
23 |
24 | ```PHP
25 | // TODO: your code
26 | ```
27 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | vendor
2 | .idea
3 | composer.lock
4 | *.sw[a-z]
5 | coverage
--------------------------------------------------------------------------------
/.scrutinizer.yml:
--------------------------------------------------------------------------------
1 | checks:
2 | php: true
3 | filter:
4 | excluded_paths:
5 | - tests/*
6 | build:
7 | environment:
8 | php: 8.2.0
9 | nodes:
10 | analysis:
11 | tests:
12 | override:
13 | - php-scrutinizer-run
14 | - phpcs-run src/*
15 | my-tests:
16 | environment:
17 | php:
18 | version: 8.2.0
19 | pecl_extensions:
20 | - swoole-4.8.13
21 | dependencies:
22 | before:
23 | - composer install
24 | tests:
25 | override:
26 | - composer test
27 | coverage:
28 | tests:
29 | override:
30 | - command: composer test
31 | coverage:
32 | file: coverage/coverage.xml
33 | format: clover
34 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: php
2 |
3 | php:
4 | - 7.4
5 | - 8.0
6 | - 8.1
7 | - 8.2
8 | - 8.3
9 |
10 | before_script:
11 | - pecl install --onlyreqdeps --nobuild swoole && cd "$(pecl config-get temp_dir)/swoole" && phpize && ./configure --enable-openssl && make -j$(nproc) && make -j$(nproc) install && cd -
12 | - composer install
13 |
14 | script: composer test
15 |
--------------------------------------------------------------------------------
/KnownIssues-CN.md:
--------------------------------------------------------------------------------
1 | # 常见问题
2 |
3 | ## Class swoole does not exist
4 | - 在`LaravelS`中,`Swoole`是以`cli`模式启动的`Http Server`,替代了`FPM`。
5 | - 投递任务、触发异步事件都会调用`app('swoole')`,从`Laravel容器`中获取`Swoole\http\server`实例。只有在`LaravelS`启动时,才会注入这个实例到容器中。
6 | - 所以,一旦脱离`LaravelS`,由于跨进程,以下情况,你将`无法`成功调用`app('swoole')`:
7 | - 以各种`命令行`方式运行的代码,例如Artisan命令行、PHP脚本命令行;
8 | - 运行在`FPM`/`Apache PHP Module`下的代码,查看SAPI `Log::info('PHP SAPI', [php_sapi_name()]);`。
9 |
10 | ## 使用包 [encore/laravel-admin](https://github.com/z-song/laravel-admin)
11 | > 修改`config/laravels.php`,在`cleaners`中增加`LaravelAdminCleaner`。
12 |
13 | ```php
14 | 'cleaners' => [
15 | Hhxsv5\LaravelS\Illuminate\Cleaners\LaravelAdminCleaner::class,
16 | ],
17 | ```
18 |
19 | ## 使用包 [jenssegers/agent](https://github.com/jenssegers/agent)
20 | > [监听系统事件](https://github.com/hhxsv5/laravel-s/blob/master/README-CN.md#%E7%B3%BB%E7%BB%9F%E4%BA%8B%E4%BB%B6)
21 |
22 | ```php
23 | // 重置Agent
24 | \Event::listen('laravels.received_request', function (\Illuminate\Http\Request $req, $app) {
25 | $app->agent->setHttpHeaders($req->server->all());
26 | $app->agent->setUserAgent();
27 | });
28 | ```
29 |
30 | ## 使用包 [barryvdh/laravel-debugbar](https://github.com/barryvdh/laravel-debugbar)
31 | > 官方不支持`cli`模式,需通过修改环境变量`APP_RUNNING_IN_CONSOLE`为非`cli`,但启用后不排除会有其他问题。
32 |
33 | `.env`中增加环境变量`APP_RUNNING_IN_CONSOLE=false`。
34 |
35 | ## 使用包 [the-control-group/voyager](https://github.com/the-control-group/voyager)
36 | > `voyager`依赖包[arrilot/laravel-widgets](https://github.com/arrilot/laravel-widgets),而其中`WidgetGroupCollection`是单例,[追加Widget](https://github.com/arrilot/laravel-widgets/blob/master/src/WidgetGroup.php#L270)会造成它们重复展示,通过重新注册ServiceProvider来重置此单例。
37 |
38 | ```php
39 | // config/laravels.php
40 | 'register_providers' => [
41 | Arrilot\Widgets\ServiceProvider::class,
42 | ],
43 | ```
44 |
45 | ## 使用包 [overtrue/wechat](https://github.com/overtrue/wechat)
46 | > easywechat包会出现异步通知回调失败的问题,原因是`$app['request']->getContent()`是空的,给其赋值即可。
47 |
48 | ```php
49 | //回调通知
50 | public function notify(Request $request)
51 | {
52 | $app = $this->getPayment();//获取支付实例
53 | $app['request'] = $request;//在原有代码添加这一行,将当前Request赋值给$app['request']
54 | $response = $app->handlePaidNotify(function ($message, $fail) use($id) {
55 | //...
56 | });
57 | return $response;
58 | }
59 | ```
60 |
61 |
62 | ## 使用包 [laracasts/flash](https://github.com/laracasts/flash)
63 | > 常驻内存后,每次调用flash()会追加消息提醒,导致叠加展示消息提醒。有以下两个方案。
64 |
65 | 1.通过中间件在每次请求`处理前`或`处理后`重置$messages `app('flash')->clear();`。
66 |
67 | 2.每次请求处理后重新注册`FlashServiceProvider`,配置[register_providers](https://github.com/hhxsv5/laravel-s/blob/master/Settings-CN.md)。
68 |
69 | ## 使用包 [laravel/telescope](https://github.com/laravel/telescope)
70 | > 因Swoole运行在`cli`模式,导致`RequestWatcher`不能正常识别忽略的路由。
71 |
72 | 解决方案:
73 |
74 | 1.`.env`中增加环境变量`APP_RUNNING_IN_CONSOLE=false`;
75 |
76 | 2.修改代码。
77 |
78 | ```php
79 | // 修改`app/Providers/EventServiceProvider.php`, 添加下面监听代码到boot方法中
80 | // use Laravel\Telescope\Telescope;
81 | // use Illuminate\Support\Facades\Event;
82 | Event::listen('laravels.received_request', function ($request, $app) {
83 | $reflection = new \ReflectionClass(Telescope::class);
84 | $handlingApprovedRequest = $reflection->getMethod('handlingApprovedRequest');
85 | $handlingApprovedRequest->setAccessible(true);
86 | $handlingApprovedRequest->invoke(null, $app) ? Telescope::startRecording() : Telescope::stopRecording();
87 | });
88 | ```
89 |
90 | ## 单例控制器
91 |
92 | - Laravel 5.3+ 控制器是被绑定在`Router`下的`Route`中,而`Router`是单例,控制器只会被构造`一次`,所以不能在构造方法中初始化`请求级数据`,下面展示`错误的用法`。
93 |
94 | ```php
95 | namespace App\Http\Controllers;
96 | class TestController extends Controller
97 | {
98 | protected $userId;
99 | public function __construct()
100 | {
101 | // 错误的用法:因控制器只被构造一次,然后常驻于内存,所以$userId只会被赋值一次,后续请求会误读取之前请求$userId
102 | $this->userId = session('userId');
103 | }
104 | public function testAction()
105 | {
106 | // 读取$this->userId;
107 | }
108 | }
109 | ```
110 |
111 | - 两种解决方法(二选一)
112 |
113 | 1.避免在构造函数中初始化`请求级`的数据,应在具体`Action`中读取,这样编码风格更合理,建议这样写。
114 |
115 | ```bash
116 | # 列出你的路由中所有关联的控制器的所有属性
117 | php artisan laravels:list-properties
118 | ```
119 |
120 | ```php
121 | namespace App\Http\Controllers;
122 | class TestController extends Controller
123 | {
124 | protected function getUserId()
125 | {
126 | return session('userId');
127 | }
128 | public function testAction()
129 | {
130 | // 通过调用$this->getUserId()读取$userId
131 | }
132 | }
133 | ```
134 |
135 | 2.使用`LaravelS`提供的`自动销毁控制器`机制。
136 |
137 | ```php
138 | // config/laravels.php
139 | // 将enable置为true、excluded_list置为[],则表示自动销毁所有控制器
140 | 'destroy_controllers' => [
141 | 'enable' => true, // 启用自动销毁控制器
142 | 'excluded_list' => [
143 | //\App\Http\Controllers\TestController::class, // 排除销毁的控制器类列表
144 | ],
145 | ],
146 | ```
147 |
148 | ## 不能使用这些函数
149 |
150 | - `flush`/`ob_flush`/`ob_end_flush`/`ob_implicit_flush`:`swoole_http_response`不支持`flush`。
151 |
152 | - `dd()`/`exit()`/`die()`: 将导致Worker/Task/Process进程立即退出,建议通过抛异常跳出函数调用栈,[Swoole文档](https://wiki.swoole.com/wiki/page/501.html)。
153 |
154 | - `header()`/`setcookie()`/`http_response_code()`:HTTP响应只能通过Laravel/Lumen的`Response`对象。
155 |
156 | ## 不能使用的全局变量
157 |
158 | - $_GET、$_POST、$_FILES、$_COOKIE、$_REQUEST、$_SESSION、$GLOBALS,$_ENV是`可读`的,$_SERVER是`部分可读`的。
159 |
160 | ## 大小限制
161 |
162 | - `Swoole`限制了`GET`请求头的最大尺寸为`8KB`,建议`Cookie`的不要太大,不然Cookie可能解析失败。
163 |
164 | - `POST`数据或文件上传的大小受`Swoole`配置[`package_max_length`](https://wiki.swoole.com/wiki/page/301.html)限制,默认上限`2M`。
165 |
166 | - 文件上传的大小受到了[memory_limit](https://www.php.net/manual/zh/ini.core.php#ini.memory-limit)的限制,默认`128M`。
167 |
168 | ## Inotify监听文件数达到上限
169 | > `Warning: inotify_add_watch(): The user limit on the total number of inotify watches was reached`
170 |
171 | - `Linux`中`Inotify`监听文件数默认上限一般是`8192`,实际项目的文件数+目录树很可能超过此上限,进而导致后续的监听失败。
172 |
173 | - 增加此上限到`524288`:`echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p`,注意`Docker`时需设置启用`privileged`。
174 |
175 | ## 注意include/require与(include/require)_once
176 | > 看看鸟哥这篇文章[再一次, 不要使用(include/require)_once](http://www.laruence.com/2012/09/12/2765.html)
177 |
178 | - 引入`类`、`接口`、`trait`、`函数`时使用(include/require)_once,其他情况使用include/require。
179 |
180 | - 在多进程模式下,子进程会继承父进程资源,一旦父进程引入了某个需要被执行的文件,子进程再次`require_once()`时会直接返回`true`,导致该文件执行失败。此时,你应该使用include/require。
181 |
182 |
183 | ## 对于`Swoole < 1.9.17`的环境
184 | > 开启`handle_static`后,静态资源文件将由`LaravelS`组件处理。由于PHP环境的原因,可能会导致`MimeTypeGuesser`无法正确识别`MimeType`,比如会Javascript与CSS文件会被识别为`text/plain`。
185 |
186 | 解决方案:
187 |
188 | 1.升级Swoole到`1.9.17+`
189 |
190 | 2.注册自定义MIME猜测器
191 |
192 | ```php
193 | // MyGuessMimeType.php
194 | use Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesserInterface;
195 | class MyGuessMimeType implements MimeTypeGuesserInterface
196 | {
197 | protected static $map = [
198 | 'js' => 'application/javascript',
199 | 'css' => 'text/css',
200 | ];
201 | public function guess($path)
202 | {
203 | $ext = pathinfo($path, PATHINFO_EXTENSION);
204 | if (strlen($ext) > 0) {
205 | return Arr::get(self::$map, $ext);
206 | } else {
207 | return null;
208 | }
209 | }
210 | }
211 | ```
212 |
213 | ```php
214 | // AppServiceProvider.php
215 | use Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesser;
216 | public function boot()
217 | {
218 | MimeTypeGuesser::getInstance()->register(new MyGuessMimeType());
219 | }
220 | ```
221 |
222 |
--------------------------------------------------------------------------------
/KnownIssues.md:
--------------------------------------------------------------------------------
1 | # Known issues
2 |
3 | ## Class swoole does not exist
4 | - In `LaravelS`, `Swoole` is `Http Server` started in `cli` mode, replacing `FPM`.
5 | - Delivering a task, triggering an asynchronous event will call `app('swoole')` and get the `Swoole\http\server` instance from the `Laravel container`. This instance is injected into the container only when `LaravelS` is started.
6 | - So, once you leave the `LaravelS`, due to the cross-process, you will be `unable` to successfully call `app('swoole')`:
7 | - The code that runs in various `command line` modes, such as the Artisan command line and the PHP script command line.
8 | - Run the code under `FPM`/`Apache PHP Module`, view SAPI `Log::info('PHP SAPI', [php_sapi_name()]);`.
9 |
10 | ## Use package [encore/laravel-admin](https://github.com/z-song/laravel-admin)
11 | > Modify `config/laravels.php` and add` LaravelAdminCleaner` in `cleaners`.
12 |
13 | ```php
14 | 'cleaners' => [
15 | Hhxsv5\LaravelS\Illuminate\Cleaners\LaravelAdminCleaner::class,
16 | ],
17 | ```
18 |
19 | ## Use package [jenssegers/agent](https://github.com/jenssegers/agent)
20 | > [Listen System Event](https://github.com/hhxsv5/laravel-s/blob/master/README.md#system-events)
21 |
22 | ```php
23 | // Reset Agent
24 | \Event::listen('laravels.received_request', function (\Illuminate\Http\Request $req, $app) {
25 | $app->agent->setHttpHeaders($req->server->all());
26 | $app->agent->setUserAgent();
27 | });
28 | ```
29 |
30 | ## Use package [barryvdh/laravel-debugbar](https://github.com/barryvdh/laravel-debugbar)
31 | > Not support `cli` mode officially, you need to add the environment variable `APP_RUNNING_IN_CONSOLE` to be non-cli`, but there may be some other issues.
32 |
33 | Add environment variable `APP_RUNNING_IN_CONSOLE=false` to `.env`.
34 |
35 | ## Use package [the-control-group/voyager](https://github.com/the-control-group/voyager)
36 | > `voyager` dependencies [arrilot/laravel-widgets](https://github.com/arrilot/laravel-widgets), where `WidgetGroupCollection` is a singleton, [appending widget](https://github.com/Arrilot/laravel-widgets/blob/master/src/WidgetGroup.php#L270) will cause them to repeat the display, you need to reset the singleton by re-registering the ServiceProvider.
37 |
38 | ```php
39 | // config/laravels.php
40 | 'register_providers' => [
41 | Arrilot\Widgets\ServiceProvider::class,
42 | ],
43 | ```
44 |
45 | ## Use package [overtrue/wechat](https://github.com/overtrue/wechat)
46 | > The asynchronous notification callback will be failing, because `$app['request']->getContent()` is empty, give it a value.
47 |
48 | ```php
49 | public function notify(Request $request)
50 | {
51 | $app = $this->getPayment();//Get payment instance
52 | $app['request'] = $request;//Add this line to the original code and assign the current request instance to $app['request']
53 | $response = $app->handlePaidNotify(function ($message, $fail) use($id) {
54 | //...
55 | });
56 | return $response;
57 | }
58 | ```
59 | ## Use package [laracasts/flash](https://github.com/laracasts/flash)
60 | > Flash messages are held in memory all the time. Appending to `$messages` when call flash() every time, leads to the multiple messages. There are two solutions.
61 |
62 | 1.Reset `$messages` by middleware `app('flash')->clear();`.
63 |
64 | 2.Re-register `FlashServiceProvider` after handling request, Refer [register_providers](https://github.com/hhxsv5/laravel-s/blob/master/Settings.md).
65 |
66 | ## Use package [laravel/telescope](https://github.com/laravel/telescope)
67 | > Because Swoole is running in `cli` mode, `RequestWatcher` does not recognize the ignored route properly.
68 |
69 | Solution:
70 |
71 | 1.Add environment variable `APP_RUNNING_IN_CONSOLE=false` to `.env`;
72 |
73 | 2.Modify code.
74 |
75 | ```php
76 | // Edit file `app/Providers/EventServiceProvider.php`, add the following code into method `boot`
77 | // use Laravel\Telescope\Telescope;
78 | // use Illuminate\Support\Facades\Event;
79 | Event::listen('laravels.received_request', function ($request, $app) {
80 | $reflection = new \ReflectionClass(Telescope::class);
81 | $handlingApprovedRequest = $reflection->getMethod('handlingApprovedRequest');
82 | $handlingApprovedRequest->setAccessible(true);
83 | $handlingApprovedRequest->invoke(null, $app) ? Telescope::startRecording() : Telescope::stopRecording();
84 | });
85 | ```
86 |
87 | ## Singleton controller
88 |
89 | - Laravel 5.3+ controller is bound to `Route` under `Router`, and `Router` is a singleton, controller will only be constructed `once`, so you cannot initialize `request-level data` in the constructor, the following shows you the `wrong` usage.
90 |
91 | ```php
92 | namespace App\Http\Controllers;
93 | class TestController extends Controller
94 | {
95 | protected $userId;
96 | public function __construct()
97 | {
98 | // Wrong usage: Since the controller is only constructed once and then resident in memory, $userId will only be assigned once, and subsequent requests will be misread before requesting $userId
99 | $this->userId = session('userId');
100 | }
101 | public function testAction()
102 | {
103 | // read $this->userId;
104 | }
105 | }
106 | ```
107 |
108 | - Two solutions (choose one)
109 |
110 | 1.Avoid initializing `request-level` data in the constructor, which should be read in the concrete `Action`. This coding style is more reasonable, it is recommended to do so.
111 |
112 | ```bash
113 | # List all properties of all controllers related your routes.
114 | php artisan laravels:list-properties
115 | ```
116 |
117 | ```php
118 | namespace App\Http\Controllers;
119 | class TestController extends Controller
120 | {
121 | protected function getUserId()
122 | {
123 | return session('userId');
124 | }
125 | public function testAction()
126 | {
127 | // call $this->getUserId() to read $userId
128 | }
129 | }
130 | ```
131 |
132 | 2.Use the `automatic destruction controller` mechanism provided by `LaravelS`.
133 |
134 | ```php
135 | // config/laravels.php
136 | // Set enable to true and exclude_list to [], which means that all controllers are automatically destroyed.
137 | 'destroy_controllers' => [
138 | 'enable' => true, // Enable automatic destruction controller
139 | 'excluded_list' => [
140 | //\App\Http\Controllers\TestController::class, // The excluded list of destroyed controller classes
141 | ],
142 | ],
143 | ```
144 |
145 | ## Cannot call these functions
146 |
147 | - `flush`/`ob_flush`/`ob_end_flush`/`ob_implicit_flush`: `swoole_http_response` does not support `flush`.
148 |
149 | - `dd()`/`exit()`/`die()`: will lead to Worker/Task/Process quit right now, suggest jump out function call stack by throwing exception.
150 |
151 | - `header()`/`setcookie()`/`http_response_code()`: Make HTTP response by Laravel/Lumen `Response` only in LaravelS underlying.
152 |
153 | ## Cannot use these global variables
154 |
155 | - $_GET/$_POST/$_FILES/$_COOKIE/$_REQUEST/$_SESSION/$GLOBALS, $_ENV is `readable`, $_SERVER is `partial readable`.
156 |
157 | ## Size restriction
158 |
159 | - The max size of `GET` request's header is `8KB`, limited by `Swoole`, the big `Cookie` will lead to parse Cookie fail.
160 |
161 | - The max size of `POST` data/file is limited by `Swoole` [package_max_length](https://www.swoole.co.uk/docs/modules/swoole-server/configuration), default `2M`.
162 |
163 | - The max size of the file upload is limited by [memory_limit](https://www.php.net/manual/en/ini.core.php#ini.memory-limit), default `128M`.
164 |
165 | ## Inotify reached the watchers limit
166 | > `Warning: inotify_add_watch(): The user limit on the total number of inotify watches was reached`
167 |
168 | - Inotify limit is default `8192` for most `Linux`, but the amount of actual project may be more than it, then lead to watch fail.
169 |
170 | - Increase the amount of inotify watchers to `524288`: `echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p`, note: you need to enable `privileged` for `Docker`.
171 |
172 | ## include/require与(include/require)_once
173 | > See Laruence's blog [Do NOT USE (include/require)_once](http://www.laruence.com/2012/09/12/2765.html)
174 |
175 | - To include the files about `class`/`interface`/`trait`/`function`, sugguest to use (include/require)_once. In other cases, use include/require.
176 |
177 | - In the multi-process mode, the child process inherits the parent process resource. Once the parent process includes a file that needs to be executed, the child process will directly return true when it uses require_once(), causing the file to fail to execute. Now, you need to use include/require.
178 |
179 | ## If `Swoole < 1.9.17`
180 | > After enabling `handle_static`, static resource files will be handled by `LaravelS`. Due to the PHP environment, `MimeTypeGuesser` may not correctly recognize `MimeType`. For example, Javascript and CSS files will be recognized as `text/plain`.
181 |
182 | Solutions:
183 |
184 | 1.Upgrade Swoole to `1.9.17+`.
185 |
186 | 2.Register a custom MIME guesser.
187 |
188 | ```php
189 | // MyGuessMimeType.php
190 | use Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesserInterface;
191 | class MyGuessMimeType implements MimeTypeGuesserInterface
192 | {
193 | protected static $map = [
194 | 'js' => 'application/javascript',
195 | 'css' => 'text/css',
196 | ];
197 | public function guess($path)
198 | {
199 | $ext = pathinfo($path, PATHINFO_EXTENSION);
200 | if (strlen($ext) > 0) {
201 | return Arr::get(self::$map, $ext);
202 | } else {
203 | return null;
204 | }
205 | }
206 | }
207 | ```
208 |
209 | ```php
210 | // AppServiceProvider.php
211 | use Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesser;
212 | public function boot()
213 | {
214 | MimeTypeGuesser::getInstance()->register(new MyGuessMimeType());
215 | }
216 | ```
217 |
218 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 hhxsv5
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Settings-CN.md:
--------------------------------------------------------------------------------
1 | # LaravelS 配置项
2 |
3 | ## listen_ip
4 | > `string` 监听的IP,监听本机`127.0.0.1`(IPv4) `::1`(IPv6),监听所有地址 `0.0.0.0`(IPv4) `::`(IPv6), 默认`127.0.0.1`。
5 |
6 | ## listen_port
7 | > `int` 监听的端口,如果端口小于1024则需要`root`权限,默认 `5200`。
8 |
9 | ## socket_type
10 | > 默认`SWOOLE_SOCK_TCP`。通常情况下,无需关心这个配置。若需Nginx代理至`UnixSocket Stream`文件,则需修改为`SWOOLE_SOCK_UNIX_STREAM`,此时`listen_ip`则是`UnixSocket Stream`文件的路径。
11 |
12 | ## server
13 | > `string` 当通过LaravelS响应数据时,设置HTTP头部`Server`的值,若为空则不设置,默认 `LaravelS`。
14 |
15 | ## handle_static
16 | > `bool` 是否开启LaravelS处理静态资源(要求 `Swoole >= 1.7.21`,若`Swoole >= 1.9.17`则由Swoole自己处理),默认`false`,建议Nginx处理静态资源,LaravelS仅处理动态资源。静态资源的默认路径为`base_path('public')`,可通过修改`swoole.document_root`变更。
17 |
18 | ## laravel_base_path
19 | > `string` `Laravel/Lumen`的基础路径,默认`base_path()`,可用于配置`符号链接`。
20 |
21 | ## inotify_reload.enable
22 | > `bool` 是否开启`Inotify Reload`,用于当修改代码后实时Reload所有worker进程,依赖库[inotify](http://pecl.php.net/package/inotify),通过命令`php --ri inotify`检查是否可用,默认`false`,`建议仅开发环境开启`,[修改监听数上限](https://github.com/hhxsv5/laravel-s/blob/master/KnownIssues-CN.md#inotify%E7%9B%91%E5%90%AC%E6%96%87%E4%BB%B6%E6%95%B0%E8%BE%BE%E5%88%B0%E4%B8%8A%E9%99%90)。
23 |
24 | ## inotify_reload.watch_path
25 | > `string` `Inotify` 监控的文件路径,默认有`base_path()`。
26 |
27 | ## inotify_reload.file_types
28 | > `array` `Inotify` 监控的文件类型,默认有`.php`。
29 |
30 | ## inotify_reload.excluded_dirs
31 | > `array` `Inotify` 监控时需要排除(或忽略)的目录,默认`[]`,示例:`[base_path('vendor')]`。
32 |
33 | ## inotify_reload.log
34 | > `bool` 是否输出Reload的日志,默认`true`。
35 |
36 | ## event_handlers
37 | > `array` 配置`Swoole`的事件回调函数,key-value格式,key为事件名,value为实现了事件处理接口的类,参考[示例](https://github.com/hhxsv5/laravel-s/blob/master/README-CN.md#%E9%85%8D%E7%BD%AEswoole%E7%9A%84%E4%BA%8B%E4%BB%B6%E5%9B%9E%E8%B0%83%E5%87%BD%E6%95%B0)。
38 |
39 | ## websocket.enable
40 | > `bool` 是否启用WebSocket服务器。启用后WebSocket服务器监听的IP和端口与Http服务器相同,默认`false`。
41 |
42 | ## websocket.handler
43 | > `string` WebSocket逻辑处理的类名,需实现接口`WebSocketHandlerInterface`,参考[示例](https://github.com/hhxsv5/laravel-s/blob/master/README-CN.md#%E5%90%AF%E7%94%A8websocket%E6%9C%8D%E5%8A%A1%E5%99%A8)。
44 |
45 | ## sockets
46 | > `array` 配置`TCP/UDP`套接字列表,参考[示例](https://github.com/hhxsv5/laravel-s/blob/master/README-CN.md#%E5%A4%9A%E7%AB%AF%E5%8F%A3%E6%B7%B7%E5%90%88%E5%8D%8F%E8%AE%AE)。
47 |
48 | ## processes
49 | > `array` 配置自定义进程列表,参考[示例](https://github.com/hhxsv5/laravel-s/blob/master/README-CN.md#%E8%87%AA%E5%AE%9A%E4%B9%89%E8%BF%9B%E7%A8%8B)。
50 |
51 | ## timer
52 | > `array` 配置毫秒定时器,参考[示例](https://github.com/hhxsv5/laravel-s/blob/master/README-CN.md#%E6%AF%AB%E7%A7%92%E7%BA%A7%E5%AE%9A%E6%97%B6%E4%BB%BB%E5%8A%A1)。
53 |
54 | ## swoole_tables
55 | > `array` 定义的`swoole_table`列表,参考[示例](https://github.com/hhxsv5/laravel-s/blob/master/README-CN.md#%E4%BD%BF%E7%94%A8swoole_table)。
56 |
57 | ## cleaners
58 | > `array` `每次请求`的清理器列表,用于清理一些残留的全局变量、单例对象、静态属性,避免多次请求间数据污染。这些清理器类必须实现接口`Hhxsv5\LaravelS\Illuminate\Cleaners\CleanerInterface`。清理的顺序与数组的顺序保持一致。[这些清理器](https://github.com/hhxsv5/laravel-s/blob/master/src/Illuminate/CleanerManager.php#L31)默认已启用,无需再配置。
59 |
60 | ```php
61 | // 如果你的项目中使用到了Session、Authentication、Passport,需配置如下清理器
62 | 'cleaners' => [
63 | Hhxsv5\LaravelS\Illuminate\Cleaners\SessionCleaner::class,
64 | Hhxsv5\LaravelS\Illuminate\Cleaners\AuthCleaner::class,
65 | ],
66 | ```
67 |
68 | ```php
69 | // 如果你的项目中使用到了包"tymon/jwt-auth",需配置如下清理器
70 | 'cleaners' => [
71 | Hhxsv5\LaravelS\Illuminate\Cleaners\SessionCleaner::class,
72 | Hhxsv5\LaravelS\Illuminate\Cleaners\AuthCleaner::class,
73 | Hhxsv5\LaravelS\Illuminate\Cleaners\JWTCleaner::class,
74 | ],
75 | ```
76 |
77 | ```php
78 | // 如果你的项目中使用到了包"spatie/laravel-menu",需配置如下清理器
79 | 'cleaners' => [
80 | Hhxsv5\LaravelS\Illuminate\Cleaners\MenuCleaner::class,
81 | ],
82 | ```
83 |
84 | ```php
85 | // 如果你的项目中使用到了包"encore/laravel-admin",需配置如下清理器
86 | 'cleaners' => [
87 | Hhxsv5\LaravelS\Illuminate\Cleaners\SessionCleaner::class,
88 | Hhxsv5\LaravelS\Illuminate\Cleaners\AuthCleaner::class,
89 | Hhxsv5\LaravelS\Illuminate\Cleaners\LaravelAdminCleaner::class,
90 | ],
91 | ```
92 |
93 | ```php
94 | // 如果你的项目中使用到了包"jqhph/dcat-admin"
95 | 'cleaners' => [
96 | Hhxsv5\LaravelS\Illuminate\Cleaners\SessionCleaner::class,
97 | Hhxsv5\LaravelS\Illuminate\Cleaners\AuthCleaner::class,
98 | Hhxsv5\LaravelS\Illuminate\Cleaners\DcatAdminCleaner::class,
99 | ],
100 | ```
101 |
102 | ```php
103 | // 如果你的项目中使用到了包"tightenco/ziggy",解决"Ziggy is not defined"
104 | 'cleaners' => [
105 | Hhxsv5\LaravelS\Illuminate\Cleaners\ZiggyCleaner::class,
106 | ],
107 | ```
108 |
109 | ## register_providers
110 | > `array` `每次请求`需要重新注册的`Service Provider`列表,若存在`boot()`方法,会自动执行。一般用于清理`注册了单例的ServiceProvider`。
111 |
112 | ```php
113 | //...
114 | 'register_providers' => [
115 | \Xxx\Yyy\XxxServiceProvider::class,
116 | ],
117 | //...
118 | ```
119 |
120 | ## destroy_controllers
121 | > `array` 每次请求后自动销毁控制器,解决单例控制器的问题,参考[示例](https://github.com/hhxsv5/laravel-s/blob/master/KnownIssues-CN.md#%E5%8D%95%E4%BE%8B%E6%8E%A7%E5%88%B6%E5%99%A8)。
122 |
123 | ## swoole
124 | > `array` Swoole的`原始`配置项,请参考[Swoole服务器配置项](https://wiki.swoole.com/#/server/setting)。
--------------------------------------------------------------------------------
/Settings.md:
--------------------------------------------------------------------------------
1 | # LaravelS Settings
2 |
3 | ## listen_ip
4 | > `string` The listening ip, like local address `127.0.0.1`(IPv4) `::1`(IPv6), all addresses `0.0.0.0`(IPv4) `::`(IPv6), default `127.0.0.1`.
5 |
6 | ## listen_port
7 | > `int` The listening port, need `root` permission if port less than `1024`, default `5200`.
8 |
9 | ## socket_type
10 | > `int` Default `SWOOLE_SOCK_TCP`. Usually, you don’t need to care about it. Unless you want Nginx to proxy to the `UnixSocket Stream` file, you need to modify it to `SWOOLE_SOCK_UNIX_STREAM`, and `listen_ip` is the path of `UnixSocket Stream` file.
11 |
12 | ## server
13 | > `string` Set HTTP header `Server` when respond by LaravelS, default `LaravelS`.
14 |
15 | ## handle_static
16 | > `bool` Whether handle the static resource by LaravelS(Require `Swoole >= 1.7.21`, Handle by Swoole if `Swoole >= 1.9.17`), default `false`, Suggest that Nginx handles the statics and LaravelS handles the dynamics. The default path of static resource is `base_path('public')`, you can modify `swoole.document_root` to change it.
17 |
18 | ## laravel_base_path
19 | > `string` The basic path of `Laravel/Lumen`, default `base_path()`, be used for `symbolic link`.
20 |
21 | ## inotify_reload.enable
22 | > `bool` Whether enable the `Inotify Reload` to reload all worker processes when your code is modified, depend on [inotify](http://pecl.php.net/package/inotify), use `php --ri inotify` to check whether the available. default `false`, `recommend to enable in development environment only`, change [Watchers Limit](https://github.com/hhxsv5/laravel-s/blob/master/KnownIssues.md#inotify-reached-the-watchers-limit).
23 |
24 | ## inotify_reload.watch_path
25 | > `string` The file path that `Inotify` watches, default `base_path()`.
26 |
27 | ## inotify_reload.file_types
28 | > `array` The file types that `Inotify` watches, default `['.php']`.
29 |
30 | ## inotify_reload.excluded_dirs
31 | > `array` The excluded/ignored directories that `Inotify` watches, default `[]`, eg: `[base_path('vendor')]`.
32 |
33 | ## inotify_reload.log
34 | > `bool` Whether output the reload log, default `true`.
35 |
36 | ## event_handlers
37 | > `array` Configure the event callback function of `Swoole`, key-value format, key is the event name, and value is the class that implements the event processing interface, refer [Demo](https://github.com/hhxsv5/laravel-s/blob/master/README.md#configuring-the-event-callback-function-of-swoole).
38 |
39 | ## websocket.enable
40 | > `bool` Whether enable WebSocket Server. The Listening address of WebSocket Sever is the same as Http Server, default `false`.
41 |
42 | ## websocket.handler
43 | > `string` The class name for WebSocket handler, needs to implement interface `WebSocketHandlerInterface`, refer [Demo](https://github.com/hhxsv5/laravel-s/blob/master/README.md#enable-websocket-server).
44 |
45 | ## sockets
46 | > `array` The socket list for TCP/UDP, refer [Demo](https://github.com/hhxsv5/laravel-s/blob/master/README.md#multi-port-mixed-protocol).
47 |
48 | ## processes
49 | > `array` The custom process list, refer [Demo](https://github.com/hhxsv5/laravel-s/blob/master/README.md#custom-process).
50 |
51 | ## timer
52 | > `array` The millisecond timer, refer [Demo](https://github.com/hhxsv5/laravel-s/blob/master/README.md#millisecond-cron-job).
53 |
54 | ## swoole_tables
55 | > `array` The defined of `swoole_table` list, refer [Demo](https://github.com/hhxsv5/laravel-s/blob/master/README.md#use-swoole_table).
56 |
57 | ## cleaners
58 | > `array` The list of cleaners for `each request` is used to clean up some residual global variables, singleton objects, and static properties to avoid data pollution between requests, these classes must implement interface `Hhxsv5\LaravelS\Illuminate\Cleaners\CleanerInterface`. The order of cleanup is consistent with the order of the arrays. [These cleaners](https://github.com/hhxsv5/laravel-s/blob/master/src/Illuminate/CleanerManager.php#L31) enabled by default, and do not need to be configured.
59 |
60 | ```php
61 | // Need to configure the following cleaners if you use the session/authentication/passport in your project
62 | 'cleaners' => [
63 | Hhxsv5\LaravelS\Illuminate\Cleaners\SessionCleaner::class,
64 | Hhxsv5\LaravelS\Illuminate\Cleaners\AuthCleaner::class,
65 | ],
66 | ```
67 |
68 | ```php
69 | // Need to configure the following cleaners if you use the package "tymon/jwt-auth" in your project
70 | 'cleaners' => [
71 | Hhxsv5\LaravelS\Illuminate\Cleaners\SessionCleaner::class,
72 | Hhxsv5\LaravelS\Illuminate\Cleaners\AuthCleaner::class,
73 | Hhxsv5\LaravelS\Illuminate\Cleaners\JWTCleaner::class,
74 | ],
75 | ```
76 |
77 | ```php
78 | // Need to configure the following cleaners if you use the package "spatie/laravel-menu" in your project
79 | 'cleaners' => [
80 | Hhxsv5\LaravelS\Illuminate\Cleaners\MenuCleaner::class,
81 | ],
82 | ```
83 |
84 | ```php
85 | // Need to configure the following cleaners if you use the package "encore/laravel-admin" in your project
86 | 'cleaners' => [
87 | Hhxsv5\LaravelS\Illuminate\Cleaners\SessionCleaner::class,
88 | Hhxsv5\LaravelS\Illuminate\Cleaners\AuthCleaner::class,
89 | Hhxsv5\LaravelS\Illuminate\Cleaners\LaravelAdminCleaner::class,
90 | ],
91 | ```
92 |
93 | ```php
94 | // Need to configure the following cleaners if you use the package "jqhph/dcat-admin" in your project
95 | 'cleaners' => [
96 | Hhxsv5\LaravelS\Illuminate\Cleaners\SessionCleaner::class,
97 | Hhxsv5\LaravelS\Illuminate\Cleaners\AuthCleaner::class,
98 | Hhxsv5\LaravelS\Illuminate\Cleaners\DcatAdminCleaner::class,
99 | ],
100 | ```
101 |
102 | ```php
103 | // Need to configure the following cleaners if you use the package "tightenco/ziggy" in your project to solve "Ziggy is not defined"
104 | 'cleaners' => [
105 | Hhxsv5\LaravelS\Illuminate\Cleaners\ZiggyCleaner::class,
106 | ],
107 | ```
108 |
109 | ## register_providers
110 | > `array` The `Service Provider` list, will be re-registered `each request`, and run method `boot()` if it exists. Usually, be used to clear the `Service Provider` which registers `Singleton` instances.
111 |
112 | ```php
113 | //...
114 | 'register_providers' => [
115 | \Xxx\Yyy\XxxServiceProvider::class,
116 | ],
117 | //...
118 | ```
119 |
120 | ## destroy_controllers
121 | > `array` Automatically destroy the controllers after each request to solve the problem of the singleton controllers, refer [Demo](https://github.com/hhxsv5/laravel-s/blob/master/KnownIssues.md#singleton-controller).
122 |
123 | ## swoole
124 | > `array` Swoole's `original` configuration items, refer [Swoole Server Configuration](https://www.swoole.co.uk/docs/modules/swoole-server/configuration).
--------------------------------------------------------------------------------
/bin/fswatch:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | WORK_DIR=$1
3 | if [ ! -n "${WORK_DIR}" ] ;then
4 | WORK_DIR="."
5 | fi
6 |
7 | echo "Restarting LaravelS..."
8 | ./bin/laravels restart -d -i
9 |
10 | echo "Starting fswatch..."
11 | LOCKING=0
12 | fswatch -e ".*" -i "\\.php$" -r ${WORK_DIR} | while read file
13 | do
14 | if [[ ! ${file} =~ .php$ ]] ;then
15 | continue
16 | fi
17 | if [ ${LOCKING} -eq 1 ] ;then
18 | echo "Reloading, skipped."
19 | continue
20 | fi
21 | echo "File ${file} has been modified."
22 | LOCKING=1
23 | ./bin/laravels reload
24 | LOCKING=0
25 | done
26 | exit 0
--------------------------------------------------------------------------------
/bin/inotify:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | WORK_DIR=$1
3 | if [ ! -n "${WORK_DIR}" ] ;then
4 | WORK_DIR="."
5 | fi
6 |
7 | echo "Restarting LaravelS..."
8 | ./bin/laravels restart -d -i
9 |
10 | echo "Starting inotifywait..."
11 | LOCKING=0
12 |
13 | inotifywait --event modify --event create --event move --event delete -mrq ${WORK_DIR} | while read file
14 |
15 | do
16 | if [[ ! ${file} =~ .php$ ]] ;then
17 | continue
18 | fi
19 | if [ ${LOCKING} -eq 1 ] ;then
20 | echo "Reloading, skipped."
21 | continue
22 | fi
23 | echo "File ${file} has been modified."
24 | LOCKING=1
25 | ./bin/laravels reload
26 | LOCKING=0
27 | done
28 | exit 0
--------------------------------------------------------------------------------
/bin/laravels:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env php
2 | prefixes[$prefix]) === false) {
49 | $this->prefixes[$prefix] = [];
50 | }
51 |
52 | // retain the base directory for the namespace prefix
53 | if ($prepend) {
54 | array_unshift($this->prefixes[$prefix], $base_dir);
55 | } else {
56 | $this->prefixes[$prefix][] = $base_dir;
57 | }
58 | }
59 |
60 | /**
61 | * Loads the class file for a given class name.
62 | *
63 | * @param string $class The fully-qualified class name.
64 | * @return mixed The mapped file name on success, or boolean false on
65 | * failure.
66 | */
67 | public function loadClass($class)
68 | {
69 | // the current namespace prefix
70 | $prefix = $class;
71 |
72 | // work backwards through the namespace names of the fully-qualified
73 | // class name to find a mapped file name
74 | while (false !== $pos = strrpos($prefix, '\\')) {
75 | // retain the trailing namespace separator in the prefix
76 | $prefix = substr($class, 0, $pos + 1);
77 |
78 | // the rest is the relative class name
79 | $relative_class = substr($class, $pos + 1);
80 |
81 | // try to load a mapped file for the prefix and relative class
82 | $mapped_file = $this->loadMappedFile($prefix, $relative_class);
83 | if ($mapped_file) {
84 | return $mapped_file;
85 | }
86 |
87 | // remove the trailing namespace separator for the next iteration
88 | // of strrpos()
89 | $prefix = rtrim($prefix, '\\');
90 | }
91 |
92 | // never found a mapped file
93 | return false;
94 | }
95 |
96 | /**
97 | * Load the mapped file for a namespace prefix and relative class.
98 | *
99 | * @param string $prefix The namespace prefix.
100 | * @param string $relative_class The relative class name.
101 | * @return mixed Boolean false if no mapped file can be loaded, or the
102 | * name of the mapped file that was loaded.
103 | */
104 | protected function loadMappedFile($prefix, $relative_class)
105 | {
106 | // are there any base directories for this namespace prefix?
107 | if (isset($this->prefixes[$prefix]) === false) {
108 | return false;
109 | }
110 |
111 | // look through base directories for this namespace prefix
112 | foreach ($this->prefixes[$prefix] as $base_dir) {
113 | // replace the namespace prefix with the base directory,
114 | // replace namespace separators with directory separators
115 | // in the relative class name, append with .php
116 | $file = $base_dir
117 | . str_replace('\\', '/', $relative_class)
118 | . '.php';
119 |
120 | // if the mapped file exists, require it
121 | if ($this->requireFile($file)) {
122 | // yes, we're done
123 | return $file;
124 | }
125 | }
126 |
127 | // never found it
128 | return false;
129 | }
130 |
131 | /**
132 | * If a file exists, require it from the file system.
133 | *
134 | * @param string $file The file to require.
135 | * @return bool True if the file exists, false if not.
136 | */
137 | public function requireFile($file)
138 | {
139 | if (file_exists($file)) {
140 | require $file;
141 | return true;
142 | }
143 | return false;
144 | }
145 | }
146 |
147 | $basePath = dirname(__DIR__);
148 | $loader = new Psr4Autoloader();
149 | $loader->register();
150 |
151 | // Register laravel-s
152 | $loader->addNamespace('Hhxsv5\LaravelS', $basePath . '/vendor/hhxsv5/laravel-s/src');
153 |
154 | // Register laravel-s dependencies
155 |
156 | // To fix issue #364 https://github.com/hhxsv5/laravel-s/issues/364
157 | $loader->addNamespace('Symfony\Polyfill\Php80', $basePath . '/vendor/symfony/polyfill-php80');
158 | $loader->requireFile($basePath . '/vendor/symfony/polyfill-php80/bootstrap.php');
159 |
160 | $loader->addNamespace('Symfony\Component\Console', $basePath . '/vendor/symfony/console');
161 | $loader->addNamespace('Symfony\Contracts\Service', $basePath . '/vendor/symfony/service-contracts');
162 | $loader->addNamespace('Symfony\Contracts', $basePath . '/vendor/symfony/contracts');
163 |
164 | $command = new Hhxsv5\LaravelS\Console\Portal($basePath);
165 | $input = new Symfony\Component\Console\Input\ArgvInput();
166 | $output = new Symfony\Component\Console\Output\ConsoleOutput();
167 | $code = $command->run($input, $output);
168 | exit($code);
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "hhxsv5/laravel-s",
3 | "type": "library",
4 | "license": "MIT",
5 | "support": {
6 | "issues": "https://github.com/hhxsv5/laravel-s/issues",
7 | "source": "https://github.com/hhxsv5/laravel-s"
8 | },
9 | "description": "🚀 LaravelS is an out-of-the-box adapter between Laravel/Lumen and Swoole.",
10 | "keywords": [
11 | "laravels",
12 | "laravel-s",
13 | "swoole",
14 | "laravel",
15 | "lumen",
16 | "async",
17 | "coroutine",
18 | "server",
19 | "http",
20 | "websocket",
21 | "tcp",
22 | "udp",
23 | "process",
24 | "task",
25 | "timer",
26 | "inotify",
27 | "performance"
28 | ],
29 | "homepage": "https://github.com/hhxsv5/laravel-s",
30 | "authors": [
31 | {
32 | "name": "Xie Biao",
33 | "email": "hhxsv5@sina.com"
34 | }
35 | ],
36 | "require": {
37 | "php": ">=8.2",
38 | "ext-curl": "*",
39 | "ext-json": "*",
40 | "ext-pcntl": "*",
41 | "swoole/ide-helper": "@dev",
42 | "symfony/console": ">=6.4.0"
43 | },
44 | "suggest": {
45 | "ext-swoole": "Coroutine-based concurrency library for PHP, require >= 1.7.19.",
46 | "ext-inotify": "Inotify, used to real-time reload."
47 | },
48 | "autoload": {
49 | "psr-4": {
50 | "Hhxsv5\\LaravelS\\": "src"
51 | }
52 | },
53 | "extra": {
54 | "laravel": {
55 | "providers": [
56 | "Hhxsv5\\LaravelS\\Illuminate\\LaravelSServiceProvider"
57 | ]
58 | }
59 | },
60 | "bin": [
61 | "bin/fswatch"
62 | ],
63 | "prefer-stable": true,
64 | "minimum-stability": "dev",
65 | "require-dev": {
66 | "phpunit/phpunit": ">=4.8.36"
67 | },
68 | "autoload-dev": {
69 | "psr-4": {
70 | "Hhxsv5\\LaravelS\\Tests\\": "tests"
71 | }
72 | },
73 | "scripts": {
74 | "test": "./vendor/bin/phpunit -c phpunit.xml"
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/config/laravels.php:
--------------------------------------------------------------------------------
1 | env('LARAVELS_LISTEN_IP', '127.0.0.1'),
25 |
26 | /*
27 | |--------------------------------------------------------------------------
28 | | The port of the server
29 | |--------------------------------------------------------------------------
30 | |
31 | | Require root privilege if port is less than 1024.
32 | |
33 | */
34 |
35 | 'listen_port' => env('LARAVELS_LISTEN_PORT', 5200),
36 |
37 | /*
38 | |--------------------------------------------------------------------------
39 | | The socket type of the server
40 | |--------------------------------------------------------------------------
41 | |
42 | | Usually, you don’t need to care about it.
43 | | Unless you want Nginx to proxy to the UnixSocket Stream file, you need
44 | | to modify it to SWOOLE_SOCK_UNIX_STREAM, and listen_ip is the path of UnixSocket Stream file.
45 | | List of socket types:
46 | | SWOOLE_SOCK_TCP: TCP
47 | | SWOOLE_SOCK_TCP6: TCP IPv6
48 | | SWOOLE_SOCK_UDP: UDP
49 | | SWOOLE_SOCK_UDP6: UDP IPv6
50 | | SWOOLE_UNIX_DGRAM: Unix socket dgram
51 | | SWOOLE_UNIX_STREAM: Unix socket stream
52 | | Enable SSL: $sock_type | SWOOLE_SSL. To enable SSL, check the configuration about SSL.
53 | | https://www.swoole.co.uk/docs/modules/swoole-server-doc
54 | | https://www.swoole.co.uk/docs/modules/swoole-server/configuration
55 | |
56 | */
57 |
58 | 'socket_type' => defined('SWOOLE_SOCK_TCP') ? SWOOLE_SOCK_TCP : 1,
59 |
60 | /*
61 | |--------------------------------------------------------------------------
62 | | Server Name
63 | |--------------------------------------------------------------------------
64 | |
65 | | This value represents the name of the server that will be
66 | | displayed in the header of each request.
67 | |
68 | */
69 |
70 | 'server' => env('LARAVELS_SERVER', ''),
71 |
72 | /*
73 | |--------------------------------------------------------------------------
74 | | Handle Static Resource
75 | |--------------------------------------------------------------------------
76 | |
77 | | Whether handle the static resource by LaravelS(Require Swoole >= 1.7.21, Handle by Swoole if Swoole >= 1.9.17).
78 | | Suggest that Nginx handles the statics and LaravelS handles the dynamics.
79 | | The default path of static resource is base_path('public'), you can modify swoole.document_root to change it.
80 | |
81 | */
82 |
83 | 'handle_static' => env('LARAVELS_HANDLE_STATIC', false),
84 |
85 | /*
86 | |--------------------------------------------------------------------------
87 | | Laravel Base Path
88 | |--------------------------------------------------------------------------
89 | |
90 | | The basic path of Laravel, default base_path(), be used for symbolic link.
91 | |
92 | */
93 |
94 | 'laravel_base_path' => env('LARAVEL_BASE_PATH', base_path()),
95 |
96 | /*
97 | |--------------------------------------------------------------------------
98 | | Inotify Reload
99 | |--------------------------------------------------------------------------
100 | |
101 | | This feature requires inotify extension.
102 | | https://github.com/hhxsv5/laravel-s#automatically-reload-after-modifying-code
103 | |
104 | */
105 |
106 | 'inotify_reload' => [
107 | // Whether enable the Inotify Reload to reload all worker processes when your code is modified.
108 | 'enable' => env('LARAVELS_INOTIFY_RELOAD', false),
109 |
110 | // The file path that Inotify watches
111 | 'watch_path' => base_path(),
112 |
113 | // The file types that Inotify watches
114 | 'file_types' => ['.php'],
115 |
116 | // The excluded/ignored directories that Inotify watches
117 | 'excluded_dirs' => [],
118 |
119 | // Whether output the reload log
120 | 'log' => true,
121 | ],
122 |
123 | /*
124 | |--------------------------------------------------------------------------
125 | | Swoole Event Handlers
126 | |--------------------------------------------------------------------------
127 | |
128 | | Configure the event callback function of Swoole, key-value format,
129 | | key is the event name, and value is the class that implements the event
130 | | processing interface.
131 | |
132 | | https://github.com/hhxsv5/laravel-s#configuring-the-event-callback-function-of-swoole
133 | |
134 | */
135 |
136 | 'event_handlers' => [],
137 |
138 | /*
139 | |--------------------------------------------------------------------------
140 | | WebSockets
141 | |--------------------------------------------------------------------------
142 | |
143 | | Swoole WebSocket Server settings.
144 | |
145 | | https://github.com/hhxsv5/laravel-s#enable-websocket-server
146 | |
147 | */
148 |
149 | 'websocket' => [
150 | 'enable' => false,
151 | // 'handler' => XxxWebSocketHandler::class,
152 | ],
153 |
154 | /*
155 | |--------------------------------------------------------------------------
156 | | Sockets - multi-port mixed protocol
157 | |--------------------------------------------------------------------------
158 | |
159 | | The socket(port) list for TCP/UDP.
160 | |
161 | | https://github.com/hhxsv5/laravel-s#multi-port-mixed-protocol
162 | |
163 | */
164 |
165 | 'sockets' => [],
166 |
167 | /*
168 | |--------------------------------------------------------------------------
169 | | Custom Process
170 | |--------------------------------------------------------------------------
171 | |
172 | | Support developers to create custom processes for monitoring,
173 | | reporting, or other special tasks.
174 | |
175 | | https://github.com/hhxsv5/laravel-s#custom-process
176 | |
177 | */
178 |
179 | 'processes' => [],
180 |
181 | /*
182 | |--------------------------------------------------------------------------
183 | | Timer
184 | |--------------------------------------------------------------------------
185 | |
186 | | Wrapper cron job base on Swoole's Millisecond Timer, replace Linux Crontab.
187 | |
188 | | https://github.com/hhxsv5/laravel-s#millisecond-cron-job
189 | |
190 | */
191 |
192 | 'timer' => [
193 | 'enable' => env('LARAVELS_TIMER', false),
194 |
195 | // The list of cron job
196 | 'jobs' => [
197 | // Enable LaravelScheduleJob to run `php artisan schedule:run` every 1 minute, replace Linux Crontab
198 | // Hhxsv5\LaravelS\Illuminate\LaravelScheduleJob::class,
199 | ],
200 |
201 | // Max waiting time of reloading
202 | 'max_wait_time' => 5,
203 |
204 | // Enable the global lock to ensure that only one instance starts the timer
205 | // when deploying multiple instances.
206 | // This feature depends on Redis https://laravel.com/docs/8.x/redis
207 | 'global_lock' => false,
208 | 'global_lock_key' => config('app.name', 'Laravel'),
209 | ],
210 |
211 | /*
212 | |--------------------------------------------------------------------------
213 | | Swoole Tables
214 | |--------------------------------------------------------------------------
215 | |
216 | | All defined tables will be created before Swoole starting.
217 | |
218 | | https://github.com/hhxsv5/laravel-s#use-swooletable
219 | |
220 | */
221 |
222 | 'swoole_tables' => [],
223 |
224 | /*
225 | |--------------------------------------------------------------------------
226 | | Re-register Providers
227 | |--------------------------------------------------------------------------
228 | |
229 | | The Service Provider list, will be re-registered each request, and run method boot()
230 | | if it exists. Usually, be used to clear the Service Provider
231 | | which registers Singleton instances.
232 | |
233 | | https://github.com/hhxsv5/laravel-s/blob/master/Settings.md#register_providers
234 | |
235 | */
236 |
237 | 'register_providers' => [],
238 |
239 | /*
240 | |--------------------------------------------------------------------------
241 | | Cleaners
242 | |--------------------------------------------------------------------------
243 | |
244 | | The list of cleaners for each request is used to clean up some residual
245 | | global variables, singleton objects, and static properties to avoid
246 | | data pollution between requests.
247 | |
248 | | https://github.com/hhxsv5/laravel-s/blob/master/Settings.md#cleaners
249 | |
250 | */
251 |
252 | 'cleaners' => [],
253 |
254 | /*
255 | |--------------------------------------------------------------------------
256 | | Destroy Controllers
257 | |--------------------------------------------------------------------------
258 | |
259 | | Automatically destroy the controllers after each request to solve
260 | | the problem of the singleton controllers.
261 | |
262 | | https://github.com/hhxsv5/laravel-s/blob/master/KnownIssues.md#singleton-controller
263 | |
264 | */
265 |
266 | 'destroy_controllers' => [
267 | 'enable' => false,
268 | 'excluded_list' => [],
269 | ],
270 |
271 | /*
272 | |--------------------------------------------------------------------------
273 | | Swoole Settings
274 | |--------------------------------------------------------------------------
275 | |
276 | | Swoole's original configuration items.
277 | |
278 | | More settings
279 | | Chinese https://wiki.swoole.com/#/server/setting
280 | | English https://www.swoole.co.uk/docs/modules/swoole-server/configuration
281 | |
282 | */
283 |
284 | 'swoole' => [
285 | 'daemonize' => env('LARAVELS_DAEMONIZE', false),
286 | 'dispatch_mode' => env('LARAVELS_DISPATCH_MODE', 3),
287 | 'worker_num' => env('LARAVELS_WORKER_NUM', 30),
288 | //'task_worker_num' => env('LARAVELS_TASK_WORKER_NUM', 10),
289 | 'task_ipc_mode' => 1,
290 | 'task_max_request' => env('LARAVELS_TASK_MAX_REQUEST', 100000),
291 | 'task_tmpdir' => @is_writable('/dev/shm/') ? '/dev/shm' : '/tmp',
292 | 'max_request' => env('LARAVELS_MAX_REQUEST', 100000),
293 | 'open_tcp_nodelay' => true,
294 | 'pid_file' => storage_path('laravels.pid'),
295 | 'log_level' => env('LARAVELS_LOG_LEVEL', 4),
296 | 'log_file' => storage_path(sprintf('logs/swoole-%s.log', date('Y-m'))),
297 | 'document_root' => base_path('public'),
298 | 'buffer_output_size' => 2 * 1024 * 1024,
299 | 'socket_buffer_size' => 8 * 1024 * 1024,
300 | 'package_max_length' => 4 * 1024 * 1024,
301 | 'reload_async' => true,
302 | 'max_wait_time' => 60,
303 | 'enable_reuse_port' => true,
304 | 'enable_coroutine' => false,
305 | 'upload_tmp_dir' => @is_writable('/dev/shm/') ? '/dev/shm' : '/tmp',
306 | 'http_compression' => env('LARAVELS_HTTP_COMPRESSION', false),
307 | ],
308 | ];
309 |
--------------------------------------------------------------------------------
/config/prometheus.php:
--------------------------------------------------------------------------------
1 | env('PROMETHEUS_ENABLE', true),
12 |
13 | /*
14 | |--------------------------------------------------------------------------
15 | | The name of the application
16 | |--------------------------------------------------------------------------
17 | | Default APP_NAME.
18 | */
19 | 'application' => env('PROMETHEUS_APP_NAME', env('APP_NAME', 'Laravel')),
20 |
21 | /*
22 | |--------------------------------------------------------------------------
23 | | The prefix of apcu keys
24 | |--------------------------------------------------------------------------
25 | |
26 | | Cannot contain any regular expression characters. Default "prom".
27 | |
28 | */
29 | 'apcu_key_prefix' => env('PROMETHEUS_APCU_KEY_PREFIX', 'prom'),
30 |
31 | /*
32 | |--------------------------------------------------------------------------
33 | | The separator of apcu keys
34 | |--------------------------------------------------------------------------
35 | |
36 | | Cannot contain any regular expression characters. Default "::".
37 | |
38 | */
39 | 'apcu_key_separator' => env('PROMETHEUS_APCU_KEY_SEPARATOR', '::'),
40 |
41 | /*
42 | |--------------------------------------------------------------------------
43 | | The max age(seconds) of apcu keys.
44 | |--------------------------------------------------------------------------
45 | |
46 | | It's TTL of apcu keys. Default 259200s(3 days).
47 | |
48 | */
49 | 'apcu_key_max_age' => env('PROMETHEUS_APCU_KEY_MAX_AGE', 259200),
50 |
51 | /*
52 | |--------------------------------------------------------------------------
53 | | The time window(seconds) of the maximum duration metric for http_server_requests_seconds_max.
54 | |--------------------------------------------------------------------------
55 | |
56 | | Default 60s.
57 | |
58 | */
59 | 'max_duration_time_window' => env('PROMETHEUS_REQUEST_MAX_DURATION_TIME_WINDOW', 60),
60 |
61 | /*
62 | |--------------------------------------------------------------------------
63 | | The ignored status codes when collecting http requests.
64 | |--------------------------------------------------------------------------
65 | |
66 | | Default "400,404,405".
67 | |
68 | */
69 | 'ignored_http_codes' => array_flip(explode(',', env('PROMETHEUS_IGNORED_HTTP_CODES', '400,404,405'))),
70 |
71 | /*
72 | |--------------------------------------------------------------------------
73 | | The interval of collecting metrics.
74 | |--------------------------------------------------------------------------
75 | |
76 | | Default 10s.
77 | |
78 | */
79 | 'collect_metrics_interval' => env('PROMETHEUS_COLLECT_METRICS_INTERVAL', 10),
80 | ];
81 |
--------------------------------------------------------------------------------
/grafana-dashboard.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hhxsv5/laravel-s/895d015c288d6c412eb0dc983dcd0690b2d8c4bf/grafana-dashboard.png
--------------------------------------------------------------------------------
/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
19 |
20 |
21 | tests
22 | tests/TestCase.php
23 |
24 |
25 |
26 |
27 | tests
28 |
29 |
30 |
31 |
32 |
33 |
34 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/sponsor.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hhxsv5/laravel-s/895d015c288d6c412eb0dc983dcd0690b2d8c4bf/sponsor.png
--------------------------------------------------------------------------------
/src/Components/Apollo/Client.php:
--------------------------------------------------------------------------------
1 | server = $settings['server'];
31 | $this->appId = $settings['app_id'];
32 | if (isset($settings['cluster'])) {
33 | $this->cluster = $settings['cluster'];
34 | }
35 | if (isset($settings['namespaces'])) {
36 | $this->namespaces = $settings['namespaces'];
37 | }
38 | if (isset($settings['client_ip'])) {
39 | $this->clientIp = $settings['client_ip'];
40 | } else {
41 | $this->clientIp = current(swoole_get_local_ip()) ?: gethostname();
42 | }
43 | if (isset($settings['pull_timeout'])) {
44 | $this->pullTimeout = (int)$settings['pull_timeout'];
45 | }
46 | if (isset($settings['backup_old_env'])) {
47 | $this->backupOldEnv = (bool)$settings['backup_old_env'];
48 | }
49 | }
50 |
51 | public static function putCommandOptionsToEnv(array $options)
52 | {
53 | $envs = [
54 | 'ENABLE_APOLLO' => !empty($options['enable-apollo']),
55 | 'APOLLO_SERVER' => $options['apollo-server'],
56 | 'APOLLO_APP_ID' => $options['apollo-app-id'],
57 | 'APOLLO_CLUSTER' => $options['apollo-cluster'],
58 | 'APOLLO_NAMESPACES' => implode(',', $options['apollo-namespaces']),
59 | 'APOLLO_CLIENT_IP' => $options['apollo-client-ip'],
60 | 'APOLLO_PULL_TIMEOUT' => $options['apollo-pull-timeout'],
61 | 'APOLLO_BACKUP_OLD_ENV' => $options['apollo-backup-old-env'],
62 | ];
63 | foreach ($envs as $key => $value) {
64 | putenv("{$key}={$value}");
65 | }
66 | }
67 |
68 | public static function createFromEnv()
69 | {
70 | if (!getenv('APOLLO_SERVER') || !getenv('APOLLO_APP_ID')) {
71 | throw new \InvalidArgumentException('Missing environment variable APOLLO_SERVER or APOLLO_APP_ID');
72 | }
73 | $settings = [
74 | 'server' => getenv('APOLLO_SERVER'),
75 | 'app_id' => getenv('APOLLO_APP_ID'),
76 | 'cluster' => ($cluster = (string)getenv('APOLLO_CLUSTER')) !== '' ? $cluster : null,
77 | 'namespaces' => ($namespaces = (string)getenv('APOLLO_NAMESPACES')) !== '' ? explode(',', $namespaces) : null,
78 | 'client_ip' => ($clientIp = (string)getenv('APOLLO_CLIENT_IP')) !== '' ? $clientIp : null,
79 | 'pull_timeout' => ($pullTimeout = (int)getenv('APOLLO_PULL_TIMEOUT')) > 0 ? $pullTimeout : null,
80 | 'backup_old_env' => ($backupOldEnv = (bool)getenv('APOLLO_BACKUP_OLD_ENV')) ? $backupOldEnv : null,
81 | ];
82 | return new static($settings);
83 | }
84 |
85 | public static function createFromCommandOptions(array $options)
86 | {
87 | if (!isset($options['apollo-server'], $options['apollo-app-id'])) {
88 | throw new \InvalidArgumentException('Missing command option apollo-server or apollo-app-id');
89 | }
90 | $settings = [
91 | 'server' => $options['apollo-server'],
92 | 'app_id' => $options['apollo-app-id'],
93 | 'cluster' => isset($options['apollo-cluster']) && $options['apollo-cluster'] !== '' ? $options['apollo-cluster'] : null,
94 | 'namespaces' => !empty($options['apollo-namespaces']) ? $options['apollo-namespaces'] : null,
95 | 'client_ip' => isset($options['apollo-client-ip']) && $options['apollo-client-ip'] !== '' ? $options['apollo-client-ip'] : null,
96 | 'pull_timeout' => isset($options['apollo-pull-timeout']) ? (int)$options['apollo-pull-timeout'] : null,
97 | 'backup_old_env' => isset($options['apollo-backup-old-env']) ? (bool)$options['apollo-backup-old-env'] : null,
98 | ];
99 | return new static($settings);
100 | }
101 |
102 | public static function attachCommandOptions(Command $command)
103 | {
104 | $command->addOption('enable-apollo', null, InputOption::VALUE_NONE, 'Whether to enable Apollo component');
105 | $command->addOption('apollo-server', null, InputOption::VALUE_OPTIONAL, 'Apollo server URL');
106 | $command->addOption('apollo-app-id', null, InputOption::VALUE_OPTIONAL, 'Apollo APP ID');
107 | $command->addOption('apollo-namespaces', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'The namespace to which the APP belongs');
108 | $command->addOption('apollo-cluster', null, InputOption::VALUE_OPTIONAL, 'The cluster to which the APP belongs');
109 | $command->addOption('apollo-client-ip', null, InputOption::VALUE_OPTIONAL, 'IP of current instance');
110 | $command->addOption('apollo-pull-timeout', null, InputOption::VALUE_OPTIONAL, 'Timeout time(seconds) when pulling configuration');
111 | $command->addOption('apollo-backup-old-env', null, InputOption::VALUE_NONE, 'Whether to backup the old configuration file when updating the configuration .env file');
112 | }
113 |
114 | public function pullBatch(array $namespaces, $withReleaseKey = false, array $options = [])
115 | {
116 | $configs = [];
117 | $uri = sprintf('%s/configs/%s/%s/', $this->server, $this->appId, $this->cluster);
118 | foreach ($namespaces as $namespace) {
119 | $url = $uri . $namespace . '?' . http_build_query([
120 | 'releaseKey' => $withReleaseKey && isset($this->releaseKeys[$namespace]) ? $this->releaseKeys[$namespace] : null,
121 | 'ip' => $this->clientIp,
122 | ]);
123 | $timeout = isset($options['timeout']) ? $options['timeout'] : $this->pullTimeout;
124 | $response = $this->httpGet($url, compact('timeout'));
125 | if ($response['statusCode'] === 200) {
126 | $json = json_decode($response['body'], true);
127 | if (is_array($json)) {
128 | $configs[$namespace] = $json;
129 | $this->releaseKeys[$namespace] = $configs[$namespace]['releaseKey'];
130 | }
131 | } elseif ($response['statusCode'] === 304) {
132 | // ignore 304
133 | }
134 |
135 | }
136 | return $configs;
137 | }
138 |
139 | public function pullAll($withReleaseKey = false, array $options = [])
140 | {
141 | return $this->pullBatch($this->namespaces, $withReleaseKey, $options);
142 | }
143 |
144 | public function pullAllAndSave($filepath, array $options = [])
145 | {
146 | $all = $this->pullAll(false, $options);
147 | if (count($all) !== count($this->namespaces)) {
148 | $lackNamespaces = array_diff($this->namespaces, array_keys($all));
149 | throw new \RuntimeException('Missing Apollo configurations for namespaces ' . implode(',', $lackNamespaces));
150 | }
151 | $configs = [];
152 | foreach ($all as $namespace => $config) {
153 | $configs[] = '# Namespace: ' . $config['namespaceName'];
154 | ksort($config['configurations']);
155 | foreach ($config['configurations'] as $key => $value) {
156 | $key = preg_replace('/[^a-zA-Z0-9_.]/', '_', $key);
157 | $configs[] = sprintf('%s=%s', $key, $value);
158 | }
159 | }
160 | if (empty($configs)) {
161 | throw new \RuntimeException('Empty Apollo configuration list');
162 | }
163 | if ($this->backupOldEnv && file_exists($filepath)) {
164 | rename($filepath, $filepath . '.' . date('YmdHis'));
165 | }
166 | $fileContent = implode(PHP_EOL, $configs);
167 | if (Context::inCoroutine()) {
168 | Coroutine::writeFile($filepath, $fileContent);
169 | } else {
170 | file_put_contents($filepath, $fileContent);
171 | }
172 | return $configs;
173 | }
174 |
175 | public function startWatchNotification(callable $callback, array $options = [])
176 | {
177 | if (!isset($options['timeout']) || $options['timeout'] < 60) {
178 | $options['timeout'] = 70;
179 | }
180 | $this->watching = true;
181 | $this->notifications = [];
182 | foreach ($this->namespaces as $namespace) {
183 | $this->notifications[$namespace] = ['namespaceName' => $namespace, 'notificationId' => -1];
184 | }
185 | while ($this->watching) {
186 | $url = sprintf('%s/notifications/v2?%s',
187 | $this->server,
188 | http_build_query([
189 | 'appId' => $this->appId,
190 | 'cluster' => $this->cluster,
191 | 'notifications' => json_encode(array_values($this->notifications)),
192 | ])
193 | );
194 | $response = $this->httpGet($url, $options);
195 |
196 | if ($response['statusCode'] === 200) {
197 | $notifications = json_decode($response['body'], true);
198 | if (empty($notifications)) {
199 | continue;
200 | }
201 | if (!empty($this->notifications) && current($this->notifications)['notificationId'] !== -1) { // Ignore the first pull
202 | $callback($notifications);
203 | }
204 | array_walk($notifications, function (&$notification) {
205 | unset($notification['messages']);
206 | });
207 | $this->notifications = array_merge($this->notifications, array_column($notifications, null, 'namespaceName'));
208 | } elseif ($response['statusCode'] === 304) {
209 | // ignore 304
210 | }
211 | }
212 | }
213 |
214 | public function stopWatchNotification()
215 | {
216 | $this->watching = false;
217 | }
218 | }
219 |
--------------------------------------------------------------------------------
/src/Components/Apollo/Process.php:
--------------------------------------------------------------------------------
1 | [
21 | 'class' => static::class,
22 | 'redirect' => false,
23 | 'pipe' => 0,
24 | 'enable' => (bool)getenv('ENABLE_APOLLO'),
25 | ],
26 | ];
27 | }
28 |
29 | public static function callback(Server $swoole, SwooleProcess $process)
30 | {
31 | $filename = base_path('.env');
32 | if (isset($_ENV['_ENV'])) {
33 | $filename .= '.' . $_ENV['_ENV'];
34 | }
35 |
36 | self::$apollo = Client::createFromEnv();
37 | self::$apollo->startWatchNotification(function (array $notifications) use ($process, $filename) {
38 | $configs = self::$apollo->pullAllAndSave($filename);
39 | app('log')->info('[ApolloProcess] Pull all configurations', $configs);
40 | Portal::runLaravelSCommand(base_path(), 'reload');
41 | if (Context::inCoroutine()) {
42 | Coroutine::sleep(5);
43 | } else {
44 | sleep(5);
45 | }
46 | });
47 | }
48 |
49 | public static function onReload(Server $swoole, SwooleProcess $process)
50 | {
51 | // Stop the process...
52 | self::$apollo->stopWatchNotification();
53 | }
54 | }
--------------------------------------------------------------------------------
/src/Components/HttpClient/SimpleHttpTrait.php:
--------------------------------------------------------------------------------
1 | true,
13 | CURLOPT_FOLLOWLOCATION => true,
14 | CURLOPT_RETURNTRANSFER => true,
15 |
16 | //int
17 | CURLOPT_MAXREDIRS => 3,
18 | CURLOPT_TIMEOUT => 5,
19 | CURLOPT_CONNECTTIMEOUT => 3,
20 | ];
21 |
22 | /**
23 | * Sends a GET request and returns a array response.
24 | * @param string $url
25 | * @param array $options
26 | * @return array
27 | */
28 | public function httpGet($url, array $options)
29 | {
30 | if (Context::inCoroutine()) {
31 | $parts = parse_url($url);
32 | $path = isset($parts['path']) ? $parts['path'] : '/';
33 | if (isset($parts['query'])) {
34 | $path .= '?' . $parts['query'];
35 | }
36 | if (isset($parts['fragment'])) {
37 | $path .= '#' . $parts['fragment'];
38 | }
39 | $client = new CoroutineClient($parts['host'], isset($parts['port']) ? $parts['port'] : 80, isset($parts['scheme']) && $parts['scheme'] === 'https');
40 | if (isset($options['timeout'])) {
41 | $client->set([
42 | 'timeout' => $options['timeout'],
43 | ]);
44 | }
45 | $client->get($path);
46 | $client->close();
47 | if ($client->errCode === 110) {
48 | return ['statusCode' => 0, 'headers' => [], 'body' => ''];
49 | }
50 | if ($client->errCode !== 0) {
51 | $msg = sprintf('Failed to send Http request(%s), errcode=%d, errmsg=%s', $url, $client->errCode, $client->errMsg);
52 | throw new \RuntimeException($msg, $client->errCode);
53 | }
54 | return ['statusCode' => $client->statusCode, 'headers' => $client->headers, 'body' => $client->body];
55 | }
56 |
57 | $handle = curl_init();
58 | $finalOptions = [
59 | CURLOPT_URL => $url,
60 | CURLOPT_HTTPGET => true,
61 | ] + $this->curlOptions;
62 | if (isset($options['timeout'])) {
63 | $finalOptions[CURLOPT_TIMEOUT] = $options['timeout'];
64 | }
65 | curl_setopt_array($handle, $finalOptions);
66 | $responseStr = curl_exec($handle);
67 | $errno = curl_errno($handle);
68 | $errmsg = curl_error($handle);
69 | // Fix: curl_errno() always return 0 when fail
70 | if ($errno !== 0 || $errmsg !== '') {
71 | curl_close($handle);
72 | $msg = sprintf('Failed to send Http request(%s), errcode=%d, errmsg=%s', $url, $errno, $errmsg);
73 | throw new \RuntimeException($msg, $errno);
74 | }
75 |
76 | $headerSize = curl_getinfo($handle, CURLINFO_HEADER_SIZE);
77 | $statusCode = curl_getinfo($handle, CURLINFO_HTTP_CODE);
78 | curl_close($handle);
79 |
80 | $header = substr($responseStr, 0, $headerSize);
81 | $body = substr($responseStr, $headerSize);
82 | $lines = explode("\n", $header);
83 | array_shift($lines); // Remove status
84 |
85 | $headers = [];
86 | foreach ($lines as $part) {
87 | $middle = explode(':', $part);
88 | $key = trim($middle[0]);
89 | if ($key === '') {
90 | continue;
91 | }
92 | if (isset($headers[$key])) {
93 | $headers[$key] = (array)$headers[$key];
94 | $headers[$key][] = isset($middle[1]) ? trim($middle[1]) : '';
95 | } else {
96 | $headers[$key] = isset($middle[1]) ? trim($middle[1]) : '';
97 | }
98 | }
99 | return ['statusCode' => $statusCode, 'headers' => $headers, 'body' => $body];
100 | }
101 | }
--------------------------------------------------------------------------------
/src/Components/MetricCollector.php:
--------------------------------------------------------------------------------
1 | config = $config;
12 | }
13 | }
--------------------------------------------------------------------------------
/src/Components/MetricCollectorInterface.php:
--------------------------------------------------------------------------------
1 | [
21 | 'class' => static::class,
22 | 'redirect' => false,
23 | 'pipe' => 0,
24 | 'enable' => (bool)config('prometheus.enable', true),
25 | ],
26 | ];
27 | }
28 |
29 | public static function callback(Server $swoole, Process $process)
30 | {
31 | /**@var SwooleProcessCollector $processCollector */
32 | $processCollector = app(SwooleProcessCollector::class);
33 | /**@var SwooleStatsCollector $swooleStatsCollector */
34 | $swooleStatsCollector = app(SwooleStatsCollector::class);
35 | /**@var SystemCollector $systemCollector */
36 | $systemCollector = app(SystemCollector::class);
37 | $workerNum = $swoole->setting['worker_num'];
38 | $taskWorkerNum = isset($swoole->setting['task_worker_num']) ? $swoole->setting['task_worker_num'] : 0;
39 | $totalNum = $workerNum + $taskWorkerNum - 1;
40 | $workerIds = range(0, $totalNum);
41 | $runJob = function () use ($swoole, $workerIds, $processCollector, $swooleStatsCollector, $systemCollector) {
42 | // Collect the metrics of Swoole stats()
43 | $swooleStatsCollector->collect();
44 |
45 | // Collect the metrics of system
46 | $systemCollector->collect();
47 |
48 | // Collect the metrics of all workers
49 | foreach ($workerIds as $workerId) {
50 | $swoole->sendMessage($processCollector, $workerId);
51 | }
52 | };
53 |
54 | $interval = config('prometheus.collect_metrics_interval', 10) * 1000;
55 | self::$timerId = Timer::tick($interval, $runJob);
56 | }
57 |
58 | public static function onReload(Server $swoole, Process $process)
59 | {
60 | Timer::clear(self::$timerId);
61 | }
62 |
63 | public static function onStop(Server $swoole, Process $process)
64 | {
65 | Timer::clear(self::$timerId);
66 | }
67 | }
--------------------------------------------------------------------------------
/src/Components/Prometheus/Collectors/HttpRequestCollector.php:
--------------------------------------------------------------------------------
1 | config['max_duration_time_window'])) {
21 | $this->config['max_duration_time_window'] = 60;
22 | }
23 |
24 | $routes = method_exists(app(), 'getRoutes') ? app()->getRoutes() : app('router')->getRoutes();
25 | if ($routes instanceof \Illuminate\Routing\RouteCollection) { // Laravel
26 | foreach ($routes->getRoutes() as $route) {
27 | $method = $route->methods()[0];
28 | $uri = '/' . ltrim($route->uri(), '/');
29 | $this->routes[$method . $uri] = $uri;
30 |
31 | $action = $route->getAction();
32 | if (is_string($action['uses'])) { // Uses
33 | $this->routesByUses[$method . $action['uses']] = $uri;
34 | } elseif ($action['uses'] instanceof Closure) { // Closure
35 | $objectId = spl_object_hash($action['uses']);
36 | $this->routesByClosure[$method . $objectId] = $uri;
37 | }
38 | }
39 | } elseif (is_array($routes)) { // Lumen
40 | $this->routes = $routes;
41 | foreach ($routes as $route) {
42 | if (isset($route['action']['uses'])) { // Uses
43 | $this->routesByUses[$route['method'] . $route['action']['uses']] = $route['uri'];
44 | }
45 | if (isset($route['action'][0]) && $route['action'][0] instanceof Closure) { // Closure
46 | $objectId = spl_object_hash($route['action'][0]);
47 | $this->routesByClosure[$route['method'] . $objectId] = $route['uri'];
48 | }
49 | }
50 | }
51 | }
52 |
53 | public function collect(array $params = [])
54 | {
55 | if (!$this->config['enable']) {
56 | return;
57 | }
58 |
59 | /**@var \Illuminate\Http\Request $request */
60 | /**@var \Illuminate\Http\Response $response */
61 | list($request, $response) = $params;
62 |
63 | $status = $response->getStatusCode();
64 | if (isset($this->config['ignored_http_codes'][$status])) {
65 | // Ignore the requests.
66 | return;
67 | }
68 |
69 | $uri = $this->findRouteUri($request);
70 | $cost = (int)round((microtime(true) - $request->server('REQUEST_TIME_FLOAT')) * 1000000); // Time unit: μs
71 |
72 | // Http Request Stats
73 | $requestLabels = http_build_query([
74 | 'method' => $request->getMethod(),
75 | 'uri' => $uri,
76 | 'status' => $status,
77 | ]);
78 |
79 | // Key Format: prefix+metric_name+metric_type+metric_labels
80 | $countKey = implode($this->config['apcu_key_separator'], [$this->config['apcu_key_prefix'], 'http_server_requests_seconds_count', 'summary', $requestLabels]);
81 | $sumKey = implode($this->config['apcu_key_separator'], [$this->config['apcu_key_prefix'], 'http_server_requests_seconds_sum', 'summary', $requestLabels]);
82 | $maxKey = implode($this->config['apcu_key_separator'], [$this->config['apcu_key_prefix'], 'http_server_requests_seconds_max', 'gauge', $requestLabels]);
83 | apcu_inc($countKey, 1, $success, $this->config['apcu_key_max_age']);
84 | apcu_inc($sumKey, $cost, $success, $this->config['apcu_key_max_age']);
85 |
86 | $round = 0;
87 | do {
88 | $round++;
89 | $lastCost = apcu_fetch($maxKey);
90 | if ($lastCost === false) {
91 | if (!apcu_add($maxKey, $cost, $this->config['max_duration_time_window'])) {
92 | continue;
93 | }
94 | break;
95 | }
96 | if ($cost <= $lastCost) {
97 | break;
98 | }
99 | if (apcu_cas($maxKey, $lastCost, $cost)) {
100 | break;
101 | }
102 | } while ($round <= self::MAX_ROUND);
103 | }
104 |
105 | protected function findRouteUri(Request $request)
106 | {
107 | $method = $request->getMethod();
108 | $uri = $request->getPathInfo();
109 | $key = $method . $uri;
110 | if (isset($this->routes[$key])) {
111 | return $uri;
112 | }
113 |
114 | $route = $request->route();
115 | if ($route instanceof \Illuminate\Routing\Route) { // Laravel
116 | $uri = $route->uri();
117 | } elseif (is_array($route)) { // Lumen
118 | if (isset($route[1]['uses'])) {
119 | $key = $method . $route[1]['uses'];
120 | if (isset($this->routesByUses[$key])) {
121 | $uri = $this->routesByUses[$key];
122 | }
123 | } elseif (isset($route[1][0]) && $route[1][0] instanceof Closure) {
124 | $key = $method . spl_object_hash($route[1][0]);
125 | if (isset($this->routesByClosure[$key])) {
126 | $uri = $this->routesByClosure[$key];
127 | }
128 | }
129 | }
130 | return $uri;
131 | }
132 | }
--------------------------------------------------------------------------------
/src/Components/Prometheus/Collectors/SwooleProcessCollector.php:
--------------------------------------------------------------------------------
1 | $params['process_id'],
14 | 'process_type' => $params['process_type'],
15 | ]);
16 |
17 | // Memory Usage
18 | $memoryMetrics = [
19 | [
20 | 'name' => 'swoole_process_memory_usage',
21 | 'type' => 'gauge',
22 | 'value' => memory_get_usage(),
23 | ],
24 | [
25 | 'name' => 'swoole_process_memory_real_usage',
26 | 'type' => 'gauge',
27 | 'value' => memory_get_usage(true),
28 | ],
29 | ];
30 |
31 | // GC Status
32 | $gcMetrics = [];
33 | if (PHP_VERSION_ID >= 70300) {
34 | $gcStatus = gc_status();
35 | $gcMetrics = [
36 | [
37 | 'name' => 'swoole_process_gc_runs',
38 | 'type' => 'gauge',
39 | 'value' => $gcStatus['runs'],
40 | ],
41 | [
42 | 'name' => 'swoole_process_gc_collected',
43 | 'type' => 'gauge',
44 | 'value' => $gcStatus['collected'],
45 | ],
46 | [
47 | 'name' => 'swoole_process_gc_threshold',
48 | 'type' => 'gauge',
49 | 'value' => $gcStatus['threshold'],
50 | ],
51 | [
52 | 'name' => 'swoole_process_gc_roots',
53 | 'type' => 'gauge',
54 | 'value' => $gcStatus['roots'],
55 | ],
56 | ];
57 | }
58 | $apcuKey = implode($this->config['apcu_key_separator'], [$this->config['apcu_key_prefix'], 'swoole_process_stats', '', $labels]);
59 | apcu_store($apcuKey, array_merge($memoryMetrics, $gcMetrics), $this->config['apcu_key_max_age']);
60 | }
61 | }
--------------------------------------------------------------------------------
/src/Components/Prometheus/Collectors/SwooleStatsCollector.php:
--------------------------------------------------------------------------------
1 | stats();
14 | // Get worker_num/task_worker_num from setting for the old Swoole.
15 | $setting = $swoole->setting;
16 | if (!isset($stats['worker_num'])) {
17 | $stats['worker_num'] = $setting['worker_num'];
18 | }
19 | if (!isset($stats['task_worker_num'])) {
20 | $stats['task_worker_num'] = isset($setting['task_worker_num']) ? $setting['task_worker_num'] : 0;
21 | }
22 | $metrics = [
23 | [
24 | 'name' => 'swoole_cpu_num',
25 | 'type' => 'gauge',
26 | 'value' => swoole_cpu_num(),
27 | ],
28 | [
29 | 'name' => 'swoole_start_time',
30 | 'type' => 'gauge',
31 | 'value' => $stats['start_time'],
32 | ],
33 | [
34 | 'name' => 'swoole_connection_num',
35 | 'type' => 'gauge',
36 | 'value' => $stats['connection_num'],
37 | ],
38 | [
39 | 'name' => 'swoole_request_count',
40 | 'type' => 'gauge',
41 | 'value' => $stats['request_count'],
42 | ],
43 | [
44 | 'name' => 'swoole_worker_num',
45 | 'type' => 'gauge',
46 | 'value' => $stats['worker_num'],
47 | ],
48 | [
49 | 'name' => 'swoole_idle_worker_num',
50 | 'type' => 'gauge',
51 | 'value' => isset($stats['idle_worker_num']) ? $stats['idle_worker_num'] : 0,
52 | ],
53 | [
54 | 'name' => 'swoole_task_worker_num',
55 | 'type' => 'gauge',
56 | 'value' => $stats['task_worker_num'],
57 | ],
58 | [
59 | 'name' => 'swoole_task_idle_worker_num',
60 | 'type' => 'gauge',
61 | 'value' => isset($stats['task_idle_worker_num']) ? $stats['task_idle_worker_num'] : 0,
62 | ],
63 | [
64 | 'name' => 'swoole_tasking_num',
65 | 'type' => 'gauge',
66 | 'value' => isset($stats['tasking_num']) ? $stats['tasking_num'] : 0,
67 | ],
68 | ];
69 | $key = implode($this->config['apcu_key_separator'], [$this->config['apcu_key_prefix'], 'swoole_stats', '', '']);
70 | apcu_store($key, $metrics, $this->config['apcu_key_max_age']);
71 | }
72 | }
--------------------------------------------------------------------------------
/src/Components/Prometheus/Collectors/SystemCollector.php:
--------------------------------------------------------------------------------
1 | 'system_load_average_1m',
15 | 'type' => 'gauge',
16 | 'value' => $load[0],
17 | ],
18 | [
19 | 'name' => 'system_load_average_5m',
20 | 'type' => 'gauge',
21 | 'value' => $load[1],
22 | ],
23 | [
24 | 'name' => 'system_load_average_15m',
25 | 'type' => 'gauge',
26 | 'value' => $load[2],
27 | ],
28 | ];
29 | $key = implode($this->config['apcu_key_separator'], [$this->config['apcu_key_prefix'], 'system_stats', '', '']);
30 | apcu_store($key, $metrics, $this->config['apcu_key_max_age']);
31 | }
32 | }
--------------------------------------------------------------------------------
/src/Components/Prometheus/Exporter.php:
--------------------------------------------------------------------------------
1 | config = $config;
14 | }
15 |
16 | public function getMetrics()
17 | {
18 | $apcSmaInfo = apcu_sma_info(true);
19 | $metrics = [
20 | [
21 | 'name' => 'apcu_seg_size',
22 | 'help' => '',
23 | 'type' => 'gauge',
24 | 'value' => $apcSmaInfo['seg_size'],
25 | ],
26 | [
27 | 'name' => 'apcu_avail_mem',
28 | 'help' => '',
29 | 'type' => 'gauge',
30 | 'value' => $apcSmaInfo['avail_mem'],
31 | ],
32 | ];
33 | foreach (new \APCuIterator('/^' . $this->config['apcu_key_prefix'] . $this->config['apcu_key_separator'] . '/') as $item) {
34 | $parts = explode($this->config['apcu_key_separator'], $item['key']);
35 | parse_str($parts[3], $labels);
36 | if (is_array($item['value'])) {
37 | foreach ($item['value'] as $metric) {
38 | $metrics[] = [
39 | 'name' => $metric['name'],
40 | 'help' => '',
41 | 'type' => $metric['type'],
42 | 'value' => $metric['value'],
43 | 'labels' => $labels,
44 | ];
45 | }
46 | } else {
47 | $metrics[] = [
48 | 'name' => $parts[1],
49 | 'help' => '',
50 | 'type' => $parts[2],
51 | 'value' => $item['value'],
52 | 'labels' => $labels,
53 | ];
54 | }
55 | }
56 | return $metrics;
57 | }
58 |
59 | public function render()
60 | {
61 | $defaultLabels = ['application' => $this->config['application']];
62 | $metrics = $this->getMetrics();
63 | $lines = [];
64 | foreach ($metrics as $metric) {
65 | $lines[] = "# HELP " . $metric['name'] . " {$metric['help']}";
66 | $lines[] = "# TYPE " . $metric['name'] . " {$metric['type']}";
67 |
68 | $metricLabels = isset($metric['labels']) ? $metric['labels'] : [];
69 | $labels = ['{'];
70 | $allLabels = array_merge($defaultLabels, $metricLabels);
71 | foreach ($allLabels as $key => $value) {
72 | $labels[] = "{$key}=\"{$value}\",";
73 | }
74 | $labels[] = '}';
75 | $labelStr = implode('', $labels);
76 | $lines[] = $metric['name'] . "$labelStr {$metric['value']}";
77 | }
78 | return implode("\n", $lines);
79 | }
80 | }
--------------------------------------------------------------------------------
/src/Components/Prometheus/RequestMiddleware.php:
--------------------------------------------------------------------------------
1 | collector = $collector;
15 | }
16 |
17 | /**
18 | * Handle an incoming request.
19 | *
20 | * @param \Illuminate\Http\Request $request
21 | * @param \Closure $next
22 | * @return mixed
23 | */
24 | public function handle($request, Closure $next)
25 | {
26 | return $next($request);
27 | }
28 |
29 | /**
30 | * Handle tasks after the response has been sent to the browser.
31 | *
32 | * @param \Illuminate\Http\Request $request
33 | * @param \Illuminate\Http\Response $response
34 | * @return void
35 | */
36 | public function terminate($request, $response)
37 | {
38 | try {
39 | $this->collector->collect([$request, $response]);
40 | } catch (\Exception $e) {
41 | app('log')->error('PrometheusMiddleware: failed to collect request metrics.', ['exception' => $e]);
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/Components/Prometheus/ServiceProvider.php:
--------------------------------------------------------------------------------
1 | publishes([
18 | __DIR__ . '/../../../config/prometheus.php' => base_path('config/prometheus.php'),
19 | ]);
20 | }
21 |
22 | public function register()
23 | {
24 | $this->mergeConfigFrom(
25 | __DIR__ . '/../../../config/prometheus.php', 'prometheus'
26 | );
27 | $this->app->singleton(RequestMiddleware::class, function ($app) {
28 | return new RequestMiddleware($app->make(HttpRequestCollector::class));
29 | });
30 | $this->app->singleton(HttpRequestCollector::class, function ($app) {
31 | return new HttpRequestCollector($app['config']->get('prometheus'));
32 | });
33 | $this->app->singleton(SwooleProcessCollector::class, function ($app) {
34 | return new SwooleProcessCollector($app['config']->get('prometheus'));
35 | });
36 | $this->app->singleton(SwooleStatsCollector::class, function ($app) {
37 | return new SwooleStatsCollector($app['config']->get('prometheus'));
38 | });
39 | $this->app->singleton(SystemCollector::class, function ($app) {
40 | return new SystemCollector($app['config']->get('prometheus'));
41 | });
42 | $this->app->singleton(Exporter::class, function ($app) {
43 | return new Exporter($app['config']->get('prometheus'));
44 | });
45 | }
46 |
47 | public function provides()
48 | {
49 | return [
50 | RequestMiddleware::class,
51 | HttpRequestCollector::class,
52 | SwooleProcessCollector::class,
53 | SwooleStatsCollector::class,
54 | SystemCollector::class,
55 | Exporter::class,
56 | ];
57 | }
58 | }
--------------------------------------------------------------------------------
/src/Components/Prometheus/TimerProcessMetricsCronJob.php:
--------------------------------------------------------------------------------
1 | collect([
25 | 'process_id' => 'timer',
26 | 'process_type' => 'timer',
27 | ]);
28 | }
29 | }
--------------------------------------------------------------------------------
/src/Illuminate/CleanerManager.php:
--------------------------------------------------------------------------------
1 | currentApp = $currentApp;
65 | $this->snapshotApp = $snapshotApp;
66 | $this->reflectionApp = new ReflectionApp($this->currentApp);
67 | $this->config = $config;
68 | $this->registerCleaners(isset($this->config['cleaners']) ? $this->config['cleaners'] : []);
69 | $this->registerCleanProviders(isset($config['register_providers']) ? $config['register_providers'] : []);
70 | $this->registerCleanControllerWhiteList(isset($this->config['destroy_controllers']['excluded_list']) ? $this->config['destroy_controllers']['excluded_list'] : []);
71 | }
72 |
73 | /**
74 | * Register singleton cleaners to application container.
75 | * @param array $cleaners
76 | */
77 | protected function registerCleaners(array $cleaners)
78 | {
79 | $this->cleaners = array_unique(array_merge($cleaners, $this->cleaners));
80 | foreach ($this->cleaners as $class) {
81 | $this->currentApp->singleton($class, function () use ($class) {
82 | $cleaner = new $class($this->currentApp, $this->snapshotApp);
83 | if (!($cleaner instanceof BaseCleaner)) {
84 | throw new \InvalidArgumentException(sprintf(
85 | '%s must extend the abstract class %s',
86 | $cleaner,
87 | BaseCleaner::class
88 | )
89 | );
90 | }
91 | return $cleaner;
92 | });
93 | }
94 | }
95 |
96 | /**
97 | * Clean app after request finished.
98 | */
99 | public function clean()
100 | {
101 | foreach ($this->cleaners as $class) {
102 | /**@var BaseCleaner $cleaner */
103 | $cleaner = $this->currentApp->make($class);
104 | $cleaner->clean();
105 | }
106 | }
107 |
108 | /**
109 | * Register providers for cleaning.
110 | *
111 | * @param array providers
112 | */
113 | protected function registerCleanProviders(array $providers = [])
114 | {
115 | $this->providers = $providers;
116 | }
117 |
118 | /**
119 | * Clean Providers.
120 | */
121 | public function cleanProviders()
122 | {
123 | $loadedProviders = $this->reflectionApp->loadedProviders();
124 |
125 | foreach ($this->providers as $provider) {
126 | if (class_exists($provider)) {
127 | if ($this->config['is_lumen']) {
128 | unset($loadedProviders[get_class(new $provider($this->currentApp))]);
129 | }
130 |
131 | switch ($this->reflectionApp->registerMethodParameterCount()) {
132 | case 1:
133 | $this->currentApp->register($provider);
134 | break;
135 | case 2:
136 | $this->currentApp->register($provider, true);
137 | break;
138 | case 3:
139 | $this->currentApp->register($provider, [], true);
140 | break;
141 | default:
142 | throw new \RuntimeException('The number of parameters of the register method is unknown.');
143 | }
144 | }
145 | }
146 |
147 | if ($this->config['is_lumen']) {
148 | $this->reflectionApp->setLoadedProviders($loadedProviders);
149 | }
150 | }
151 |
152 | /**
153 | * Register white list of controllers for cleaning.
154 | *
155 | * @param array providers
156 | */
157 | protected function registerCleanControllerWhiteList(array $controllers = [])
158 | {
159 | $controllers = array_unique($controllers);
160 | $this->whiteListControllers = array_combine($controllers, $controllers);
161 | }
162 |
163 | /**
164 | * Clean controllers.
165 | */
166 | public function cleanControllers()
167 | {
168 | if ($this->config['is_lumen']) {
169 | return;
170 | }
171 |
172 | if (empty($this->config['destroy_controllers']['enable'])) {
173 | return;
174 | }
175 |
176 | /**@var \Illuminate\Routing\Route $route */
177 | $route = $this->currentApp['router']->current();
178 | if (!$route) {
179 | return;
180 | }
181 |
182 | if (isset($route->controller)) { // For Laravel 5.4+
183 | if (empty($this->whiteListControllers) || !isset($this->whiteListControllers[get_class($route->controller)])) {
184 | unset($route->controller);
185 | }
186 | } else {
187 | $reflection = new \ReflectionClass(get_class($route));
188 | if ($reflection->hasProperty('controller')) { // Laravel 5.3
189 | $controller = $reflection->getProperty('controller');
190 | $controller->setAccessible(true);
191 | if (empty($this->whiteListControllers) || (($instance = $controller->getValue($route)) && !isset($this->whiteListControllers[get_class($instance)]))) {
192 | $controller->setValue($route, null);
193 | }
194 | }
195 | }
196 | }
197 | }
198 |
--------------------------------------------------------------------------------
/src/Illuminate/Cleaners/AuthCleaner.php:
--------------------------------------------------------------------------------
1 | currentApp['auth'])) {
18 | return;
19 | }
20 | $ref = new \ReflectionObject($this->currentApp['auth']);
21 | if ($ref->hasProperty('guards')) {
22 | $this->guards = $ref->getProperty('guards');
23 | } else {
24 | $this->guards = $ref->getProperty('drivers');
25 | }
26 | $this->guards->setAccessible(true);
27 | }
28 |
29 | public function clean()
30 | {
31 | if (!isset($this->currentApp['auth'])) {
32 | return;
33 | }
34 | $this->guards->setValue($this->currentApp['auth'], []);
35 | $this->currentApp->forgetInstance('auth.driver');
36 | Facade::clearResolvedInstance('auth.driver');
37 | }
38 | }
--------------------------------------------------------------------------------
/src/Illuminate/Cleaners/BaseCleaner.php:
--------------------------------------------------------------------------------
1 | currentApp = $currentApp;
15 | $this->snapshotApp = $snapshotApp;
16 | }
17 | }
--------------------------------------------------------------------------------
/src/Illuminate/Cleaners/CleanerInterface.php:
--------------------------------------------------------------------------------
1 | currentApp['config']->set($this->snapshotApp['config']->all());
10 | }
11 | }
--------------------------------------------------------------------------------
/src/Illuminate/Cleaners/ContainerCleaner.php:
--------------------------------------------------------------------------------
1 | Initial value
12 | 'reboundCallbacks' => [],
13 | 'currentRoute' => [], // For Lumen: fixed wrong $request->route()
14 | ];
15 |
16 | private $cleanProperties = [
17 | // Property => ReflectionObject
18 | ];
19 |
20 | public function __construct(Container $currentApp, Container $snapshotApp)
21 | {
22 | parent::__construct($currentApp, $snapshotApp);
23 | $currentReflection = new \ReflectionObject($this->currentApp);
24 | $defaultValues = $currentReflection->getDefaultProperties();
25 | foreach ($this->properties as $property => &$initValue) {
26 | if ($currentReflection->hasProperty($property)) {
27 | $this->cleanProperties[$property] = $currentReflection->getProperty($property);
28 | $this->cleanProperties[$property]->setAccessible(true);
29 | $initValue = $defaultValues[$property];
30 | }
31 | }
32 | unset($initValue);
33 | }
34 |
35 | public function clean()
36 | {
37 | foreach ($this->cleanProperties as $property => $reflection) {
38 | $reflection->setValue($this->currentApp, $this->properties[$property]);
39 | }
40 | }
41 | }
--------------------------------------------------------------------------------
/src/Illuminate/Cleaners/CookieCleaner.php:
--------------------------------------------------------------------------------
1 | currentApp['cookie'])) {
15 | return;
16 | }
17 | $ref = new \ReflectionObject($this->currentApp['cookie']);
18 | $this->queued = $ref->getProperty('queued');
19 | $this->queued->setAccessible(true);
20 | }
21 |
22 | public function clean()
23 | {
24 | if (!isset($this->currentApp['cookie'])) {
25 | return;
26 | }
27 | $this->queued->setValue($this->currentApp['cookie'], []);
28 | }
29 | }
--------------------------------------------------------------------------------
/src/Illuminate/Cleaners/DcatAdminCleaner.php:
--------------------------------------------------------------------------------
1 | instances as $instance) {
28 | $this->currentApp->forgetInstance($instance);
29 | Facade::clearResolvedInstance($instance);
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/Illuminate/Cleaners/JWTCleaner.php:
--------------------------------------------------------------------------------
1 | instances as $instance) {
20 | $this->currentApp->forgetInstance($instance);
21 | Facade::clearResolvedInstance($instance);
22 | }
23 | }
24 | }
--------------------------------------------------------------------------------
/src/Illuminate/Cleaners/LaravelAdminCleaner.php:
--------------------------------------------------------------------------------
1 | [],
16 | 'script' => [],
17 | 'style' => [],
18 | 'css' => [],
19 | 'js' => [],
20 | 'html' => [],
21 | 'headerJs' => [],
22 | 'manifest' => 'vendor/laravel-admin/minify-manifest.json',
23 | 'manifestData' => [],
24 | 'extensions' => [],
25 | 'minifyIgnores' => [],
26 | 'metaTitle' => null,
27 | 'favicon' => null,
28 | 'bootingCallbacks' => [],
29 | 'bootedCallbacks' => [],
30 | ];
31 |
32 | public function __construct(Container $currentApp, Container $snapshotApp)
33 | {
34 | parent::__construct($currentApp, $snapshotApp);
35 | $this->reflection = new \ReflectionClass(self::ADMIN_CLASS);
36 | }
37 |
38 | public function clean()
39 | {
40 | foreach ($this->properties as $name => $value) {
41 | if ($this->reflection->hasProperty($name)) {
42 | $property = $this->reflection->getProperty($name);
43 | if ($property->isStatic()) {
44 | if (!$property->isPublic()) {
45 | $property->setAccessible(true);
46 | }
47 | $property->setValue($value);
48 | }
49 | }
50 | }
51 | $this->currentApp->forgetInstance(self::ADMIN_CLASS);
52 | Facade::clearResolvedInstance(self::ADMIN_CLASS);
53 | }
54 | }
--------------------------------------------------------------------------------
/src/Illuminate/Cleaners/MenuCleaner.php:
--------------------------------------------------------------------------------
1 | currentApp->forgetInstance('Lavary\Menu\Menu');
12 | Facade::clearResolvedInstance('Lavary\Menu\Menu');
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/Illuminate/Cleaners/RequestCleaner.php:
--------------------------------------------------------------------------------
1 | currentApp->forgetInstance('url');
12 | Facade::clearResolvedInstance('url');
13 |
14 | $this->currentApp->forgetInstance('request');
15 | Facade::clearResolvedInstance('request');
16 | }
17 | }
--------------------------------------------------------------------------------
/src/Illuminate/Cleaners/SessionCleaner.php:
--------------------------------------------------------------------------------
1 | currentApp['session'])) {
19 | return;
20 | }
21 | $ref = new \ReflectionObject($this->currentApp['session']);
22 | $this->drivers = $ref->getProperty('drivers');
23 | $this->drivers->setAccessible(true);
24 |
25 | }
26 |
27 | public function clean()
28 | {
29 | if (!isset($this->currentApp['session'])) {
30 | return;
31 | }
32 |
33 | $this->drivers->setValue($this->currentApp['session'], []);
34 | $this->currentApp->forgetInstance('session.store');
35 | Facade::clearResolvedInstance('session.store');
36 |
37 | if (isset($this->currentApp['redirect'])) {
38 | /**@var Redirector $redirect */
39 | $redirect = $this->currentApp['redirect'];
40 | $redirect->setSession($this->currentApp->make('session.store'));
41 | }
42 | }
43 | }
--------------------------------------------------------------------------------
/src/Illuminate/Cleaners/ZiggyCleaner.php:
--------------------------------------------------------------------------------
1 | true,
33 | '/.htaccess' => true,
34 | '/web.config' => true,
35 | ];
36 | /**@var array */
37 | protected static $staticIndexList = [
38 | 'index.html',
39 | ];
40 |
41 | /**@var array */
42 | private $rawGlobals = [];
43 |
44 | /**@var CleanerManager */
45 | protected $cleanerManager;
46 |
47 | public function __construct(array $conf = [])
48 | {
49 | $this->conf = $conf;
50 |
51 | // Merge $_ENV $_SERVER
52 | $this->rawGlobals['_SERVER'] = $_SERVER + $this->conf['_SERVER'];
53 | $this->rawGlobals['_ENV'] = $_ENV + $this->conf['_ENV'];
54 | }
55 |
56 | public function prepareLaravel()
57 | {
58 | list($this->currentApp, $this->kernel) = $this->createAppKernel();
59 |
60 | $this->reflectionApp = new ReflectionApp($this->currentApp);
61 |
62 | $this->saveSnapshot();
63 |
64 | // Create cleaner manager
65 | $this->cleanerManager = new CleanerManager($this->currentApp, $this->snapshotApp, $this->conf);
66 | }
67 |
68 | protected function saveSnapshot()
69 | {
70 | $this->snapshotApp = clone $this->currentApp;
71 |
72 | $instances = $this->reflectionApp->instances();
73 |
74 | foreach ($instances as $key => $value) {
75 | $this->snapshotApp->offsetSet($key, is_object($value) ? clone $value : $value);
76 | }
77 | }
78 |
79 | protected function createAppKernel()
80 | {
81 | // Register autoload
82 | self::autoload($this->conf['root_path']);
83 |
84 | // Make kernel for Laravel
85 | $app = require $this->conf['root_path'] . '/bootstrap/app.php';
86 | $kernel = $this->conf['is_lumen'] ? null : $app->make(HttpKernel::class);
87 |
88 | // Boot
89 | if ($this->conf['is_lumen']) {
90 | $this->configureLumen($app);
91 | if (method_exists($app, 'boot')) {
92 | $app->boot();
93 | }
94 | } else {
95 | $app->make(ConsoleKernel::class)->bootstrap();
96 | }
97 |
98 | return [$app, $kernel];
99 | }
100 |
101 | protected function configureLumen(Container $app)
102 | {
103 | $cfgPaths = [
104 | // Framework default configuration
105 | $this->conf['root_path'] . '/vendor/laravel/lumen-framework/config/',
106 | // App configuration
107 | $this->conf['root_path'] . '/config/',
108 | ];
109 |
110 | $keys = [];
111 | foreach ($cfgPaths as $cfgPath) {
112 | $configs = (array)glob($cfgPath . '*.php');
113 | foreach ($configs as $config) {
114 | $config = substr(basename($config), 0, -4);
115 | $keys[$config] = $config;
116 | }
117 | }
118 |
119 | foreach ($keys as $key) {
120 | $app->configure($key);
121 | }
122 | }
123 |
124 | public static function autoload($rootPath)
125 | {
126 | $autoload = $rootPath . '/bootstrap/autoload.php';
127 | if (file_exists($autoload)) {
128 | require_once $autoload;
129 | } else {
130 | require_once $rootPath . '/vendor/autoload.php';
131 | }
132 | }
133 |
134 | public function getRawGlobals()
135 | {
136 | return $this->rawGlobals;
137 | }
138 |
139 | public function handleDynamic(IlluminateRequest $request)
140 | {
141 | ob_start();
142 |
143 | if ($this->conf['is_lumen']) {
144 | $response = $this->currentApp->dispatch($request);
145 | if ($response instanceof SymfonyResponse) {
146 | $content = $response->getContent();
147 | } else {
148 | $content = $response;
149 | }
150 |
151 | $this->reflectionApp->callTerminableMiddleware($response);
152 | } else {
153 | $response = $this->kernel->handle($request);
154 | $content = $response->getContent();
155 | $this->kernel->terminate($request, $response);
156 | }
157 |
158 | // prefer content in response, secondly ob
159 | if (!($response instanceof StreamedResponse) && (string)$content === '' && ob_get_length() > 0) {
160 | $response->setContent(ob_get_contents());
161 | }
162 |
163 | ob_end_clean();
164 |
165 | return $response;
166 | }
167 |
168 | public function handleStatic(IlluminateRequest $request)
169 | {
170 | $uri = $request->getRequestUri();
171 | $uri = (string)str_replace("\0", '', urldecode($uri));
172 | if (isset(self::$staticBlackList[$uri]) || strpos($uri, '/..') !== false) {
173 | return false;
174 | }
175 |
176 | $requestFile = $this->conf['static_path'] . $uri;
177 | if (is_file($requestFile)) {
178 | return $this->createStaticResponse($requestFile, $request);
179 | }
180 | if (is_dir($requestFile)) {
181 | $indexFile = $this->lookupIndex($requestFile);
182 | if ($indexFile === false) {
183 | return false;
184 | }
185 | return $this->createStaticResponse($indexFile, $request);
186 | }
187 | return false;
188 | }
189 |
190 | protected function lookupIndex($folder)
191 | {
192 | $folder = rtrim($folder, '/') . '/';
193 | foreach (self::$staticIndexList as $index) {
194 | $tmpFile = $folder . $index;
195 | if (is_file($tmpFile)) {
196 | return $tmpFile;
197 | }
198 | }
199 | return false;
200 | }
201 |
202 | public function createStaticResponse($requestFile, IlluminateRequest $request)
203 | {
204 | $response = new BinaryFileResponse($requestFile);
205 | $response->prepare($request);
206 | $response->isNotModified($request);
207 | return $response;
208 | }
209 |
210 | public function clean()
211 | {
212 | $this->cleanerManager->clean();
213 | $this->cleanerManager->cleanControllers();
214 | }
215 |
216 | public function cleanProviders()
217 | {
218 | $this->cleanerManager->cleanProviders();
219 | }
220 |
221 | public function fireEvent($name, array $params = [])
222 | {
223 | $params[] = $this->currentApp;
224 | return method_exists($this->currentApp['events'], 'dispatch') ?
225 | $this->currentApp['events']->dispatch($name, $params) : $this->currentApp['events']->fire($name, $params);
226 | }
227 |
228 | public function bindRequest(IlluminateRequest $request)
229 | {
230 | $this->currentApp->instance('request', $request);
231 | }
232 |
233 | public function bindSwoole($swoole)
234 | {
235 | $this->currentApp->singleton('swoole', function () use ($swoole) {
236 | return $swoole;
237 | });
238 | }
239 |
240 | public function saveSession()
241 | {
242 | if (isset($this->currentApp['session'])) {
243 | $this->currentApp['session']->save();
244 | }
245 | }
246 | }
247 |
--------------------------------------------------------------------------------
/src/Illuminate/LaravelSCommand.php:
--------------------------------------------------------------------------------
1 | handle();
22 | }
23 |
24 | public function handle()
25 | {
26 | $action = (string)$this->argument('action');
27 | switch ($action) {
28 | case 'publish':
29 | $this->publish();
30 | break;
31 | case 'config':
32 | case 'info':
33 | $this->prepareConfig();
34 | $this->showInfo();
35 | break;
36 | default:
37 | $this->info(sprintf('Usage: [%s] ./artisan laravels publish|config|info', PHP_BINARY));
38 | if (in_array($action, ['start', 'stop', 'restart', 'reload'], true)) {
39 | $this->error(sprintf(
40 | 'The "%s" command has been migrated to "bin/laravels", %ssee https://github.com/hhxsv5/laravel-s#run',
41 | $action,
42 | file_exists(base_path('bin/laravels')) ? '' : 'please run `php artisan laravels publish` first, '
43 | ));
44 | }
45 | break;
46 | }
47 | }
48 |
49 | protected function isLumen()
50 | {
51 | return stripos($this->getApplication()->getVersion(), 'Lumen') !== false;
52 | }
53 |
54 | protected function loadConfig()
55 | {
56 | // Load configuration laravel.php manually for Lumen
57 | $basePath = config('laravels.laravel_base_path') ?: base_path();
58 | if ($this->isLumen() && file_exists($basePath . '/config/laravels.php')) {
59 | $this->getLaravel()->configure('laravels');
60 | }
61 | }
62 |
63 | protected function showInfo()
64 | {
65 | $this->showLogo();
66 | $this->showComponents();
67 | $this->showProtocols();
68 | $this->comment('>>> Feedback: https://github.com/hhxsv5/laravel-s>');
69 | }
70 |
71 | protected function showLogo()
72 | {
73 | static $logo = <<info($logo);
83 | $this->info('Speed up your Laravel/Lumen');
84 | }
85 |
86 | protected function showComponents()
87 | {
88 | $this->comment('>>> Components');
89 | $laravelSVersion = '-';
90 | $lockFile = base_path('composer.lock');
91 | $cfg = file_exists($lockFile) ? json_decode(file_get_contents($lockFile), true) : [];
92 | if (isset($cfg['packages'])) {
93 | $packages = array_merge($cfg['packages'], Arr::get($cfg, 'packages-dev', []));
94 | foreach ($packages as $package) {
95 | if (isset($package['name']) && $package['name'] === 'hhxsv5/laravel-s') {
96 | $laravelSVersion = ltrim($package['version'], 'vV');
97 | break;
98 | }
99 | }
100 | }
101 | $this->table(['Component', 'Version'], [
102 | [
103 | 'PHP',
104 | PHP_VERSION,
105 | ],
106 | [
107 | extension_loaded('openswoole') ? 'Open Swoole' : 'Swoole',
108 | SWOOLE_VERSION,
109 | ],
110 | [
111 | 'LaravelS',
112 | $laravelSVersion,
113 | ],
114 | [
115 | $this->getApplication()->getName() . ' [' . env('APP_ENV', config('app.env')) . ']',
116 | $this->getApplication()->getVersion(),
117 | ],
118 | ]);
119 | }
120 |
121 | protected function showProtocols()
122 | {
123 | $this->comment('>>> Protocols');
124 |
125 | $config = unserialize((string)file_get_contents($this->getConfigPath()));
126 | $ssl = isset($config['server']['swoole']['ssl_key_file'], $config['server']['swoole']['ssl_cert_file']);
127 | $socketType = isset($config['server']['socket_type']) ? $config['server']['socket_type'] : SWOOLE_SOCK_TCP;
128 | if (in_array($socketType, [SWOOLE_SOCK_UNIX_DGRAM, SWOOLE_SOCK_UNIX_STREAM])) {
129 | $listenAt = $config['server']['listen_ip'];
130 | } else {
131 | $listenAt = sprintf('%s:%s', $config['server']['listen_ip'], $config['server']['listen_port']);
132 | }
133 |
134 | $tableRows = [
135 | [
136 | 'Main HTTP',
137 | 'On',
138 | $this->isLumen() ? 'Lumen Router' : 'Laravel Router',
139 | sprintf('%s://%s', $ssl ? 'https' : 'http', $listenAt),
140 | ],
141 | ];
142 | if (!empty($config['server']['websocket']['enable'])) {
143 | $tableRows [] = [
144 | 'Main WebSocket',
145 | 'On',
146 | $config['server']['websocket']['handler'],
147 | sprintf('%s://%s', $ssl ? 'wss' : 'ws', $listenAt),
148 | ];
149 | }
150 |
151 | $socketTypeNames = [
152 | SWOOLE_SOCK_TCP => 'TCP IPV4 Socket',
153 | SWOOLE_SOCK_TCP6 => 'TCP IPV6 Socket',
154 | SWOOLE_SOCK_UDP => 'UDP IPV4 Socket',
155 | SWOOLE_SOCK_UDP6 => 'TCP IPV6 Socket',
156 | SWOOLE_SOCK_UNIX_DGRAM => 'Unix Socket Dgram',
157 | SWOOLE_SOCK_UNIX_STREAM => 'Unix Socket Stream',
158 | ];
159 | $sockets = isset($config['server']['sockets']) ? $config['server']['sockets'] : [];
160 | foreach ($sockets as $key => $socket) {
161 | if (isset($socket['enable']) && !$socket['enable']) {
162 | continue;
163 | }
164 |
165 | $name = 'Port#' . $key . ' ';
166 | $name .= isset($socketTypeNames[$socket['type']]) ? $socketTypeNames[$socket['type']] : 'Unknown socket';
167 | $tableRows [] = [
168 | $name,
169 | 'On',
170 | $socket['handler'],
171 | sprintf('%s:%s', $socket['host'], $socket['port']),
172 | ];
173 | }
174 | $this->table(['Protocol', 'Status', 'Handler', 'Listen At'], $tableRows);
175 | }
176 |
177 | protected function prepareConfig()
178 | {
179 | $this->loadConfig();
180 |
181 | $svrConf = config('laravels');
182 |
183 | $this->preSet($svrConf);
184 |
185 | $ret = $this->preCheck($svrConf);
186 | if ($ret !== 0) {
187 | return $ret;
188 | }
189 |
190 | // Fixed $_ENV['APP_ENV']
191 | if (isset($_SERVER['APP_ENV'])) {
192 | $_ENV['APP_ENV'] = $_SERVER['APP_ENV'];
193 | }
194 |
195 | $laravelConf = [
196 | 'root_path' => $svrConf['laravel_base_path'],
197 | 'static_path' => $svrConf['swoole']['document_root'],
198 | 'cleaners' => array_unique((array)Arr::get($svrConf, 'cleaners', [])),
199 | 'register_providers' => array_unique((array)Arr::get($svrConf, 'register_providers', [])),
200 | 'destroy_controllers' => Arr::get($svrConf, 'destroy_controllers', []),
201 | 'is_lumen' => $this->isLumen(),
202 | '_SERVER' => $_SERVER,
203 | '_ENV' => $_ENV,
204 | ];
205 |
206 | $config = ['server' => $svrConf, 'laravel' => $laravelConf];
207 | return file_put_contents($this->getConfigPath(), serialize($config)) > 0 ? 0 : 1;
208 | }
209 |
210 | protected function getConfigPath()
211 | {
212 | return storage_path('laravels.conf');
213 | }
214 |
215 | protected function preSet(array &$svrConf)
216 | {
217 | if (!isset($svrConf['enable_gzip'])) {
218 | $svrConf['enable_gzip'] = false;
219 | }
220 | if (empty($svrConf['laravel_base_path'])) {
221 | $svrConf['laravel_base_path'] = base_path();
222 | }
223 | if (empty($svrConf['process_prefix'])) {
224 | $svrConf['process_prefix'] = trim(config('app.name', '') . ' ' . $svrConf['laravel_base_path']);
225 | }
226 | if ($this->option('ignore')) {
227 | $svrConf['ignore_check_pid'] = true;
228 | } elseif (!isset($svrConf['ignore_check_pid'])) {
229 | $svrConf['ignore_check_pid'] = false;
230 | }
231 | if (empty($svrConf['swoole']['document_root'])) {
232 | $svrConf['swoole']['document_root'] = $svrConf['laravel_base_path'] . '/public';
233 | }
234 | if ($this->option('daemonize')) {
235 | $svrConf['swoole']['daemonize'] = true;
236 | } elseif (!isset($svrConf['swoole']['daemonize'])) {
237 | $svrConf['swoole']['daemonize'] = false;
238 | }
239 | if (empty($svrConf['swoole']['pid_file'])) {
240 | $svrConf['swoole']['pid_file'] = storage_path('laravels.pid');
241 | }
242 | if (empty($svrConf['timer']['max_wait_time'])) {
243 | $svrConf['timer']['max_wait_time'] = 5;
244 | }
245 |
246 | // Configure TimerProcessMetricsCronJob automatically
247 | if (isset($svrConf['processes']) && !empty($svrConf['timer']['enable'])) {
248 | foreach ($svrConf['processes'] as $process) {
249 | if ($process['class'] === CollectorProcess::class && (!isset($process['enable']) || $process['enable'])) {
250 | $svrConf['timer']['jobs'][] = TimerProcessMetricsCronJob::class;
251 | break;
252 | }
253 | }
254 | }
255 |
256 | // Set X-Version
257 | $xVersion = (string)$this->option('x-version');
258 | if ($xVersion !== '') {
259 | $_SERVER['X_VERSION'] = $_ENV['X_VERSION'] = $xVersion;
260 | }
261 | return 0;
262 | }
263 |
264 | protected function preCheck(array $svrConf)
265 | {
266 | if (!empty($svrConf['enable_gzip']) && version_compare(SWOOLE_VERSION, '4.1.0', '>=')) {
267 | $this->error('enable_gzip is DEPRECATED since Swoole 4.1.0, set http_compression of Swoole instead, http_compression is disabled by default.');
268 | $this->info('If there is a proxy server like Nginx, suggest that enable gzip in Nginx and disable gzip in Swoole, to avoid the repeated gzip compression for response.');
269 | return 1;
270 | }
271 | if (!empty($svrConf['events'])) {
272 | if (empty($svrConf['swoole']['task_worker_num']) || $svrConf['swoole']['task_worker_num'] <= 0) {
273 | $this->error('Asynchronous event listening needs to set task_worker_num > 0');
274 | return 1;
275 | }
276 | }
277 | return 0;
278 | }
279 |
280 |
281 | public function publish()
282 | {
283 | $basePath = config('laravels.laravel_base_path') ?: base_path();
284 | $configPath = $basePath . '/config/laravels.php';
285 | $todoList = [
286 | [
287 | 'from' => realpath(__DIR__ . '/../../config/laravels.php'),
288 | 'to' => $configPath,
289 | 'mode' => 0644,
290 | ],
291 | [
292 | 'from' => realpath(__DIR__ . '/../../bin/laravels'),
293 | 'to' => $basePath . '/bin/laravels',
294 | 'mode' => 0755,
295 | 'link' => true,
296 | ],
297 | [
298 | 'from' => realpath(__DIR__ . '/../../bin/fswatch'),
299 | 'to' => $basePath . '/bin/fswatch',
300 | 'mode' => 0755,
301 | 'link' => true,
302 | ],
303 | [
304 | 'from' => realpath(__DIR__ . '/../../bin/inotify'),
305 | 'to' => $basePath . '/bin/inotify',
306 | 'mode' => 0755,
307 | 'link' => true,
308 | ],
309 | ];
310 | if (file_exists($configPath)) {
311 | $choice = $this->anticipate($configPath . ' already exists, do you want to override it ? Y/N',
312 | ['Y', 'N'],
313 | 'N'
314 | );
315 | if (!$choice || strtoupper($choice) !== 'Y') {
316 | array_shift($todoList);
317 | }
318 | }
319 |
320 | foreach ($todoList as $todo) {
321 | $toDir = dirname($todo['to']);
322 | if (!is_dir($toDir) && !mkdir($toDir, 0755, true) && !is_dir($toDir)) {
323 | throw new \RuntimeException(sprintf('Directory "%s" was not created', $toDir));
324 | }
325 | if (file_exists($todo['to'])) {
326 | unlink($todo['to']);
327 | }
328 | $operation = 'Copied';
329 | if (empty($todo['link'])) {
330 | copy($todo['from'], $todo['to']);
331 | } elseif (@link($todo['from'], $todo['to'])) {
332 | $operation = 'Linked';
333 | } else {
334 | copy($todo['from'], $todo['to']);
335 | }
336 | chmod($todo['to'], $todo['mode']);
337 | $this->line("{$operation} file [{$todo['from']}] To [{$todo['to']}]");
338 | }
339 | return 0;
340 | }
341 | }
342 |
--------------------------------------------------------------------------------
/src/Illuminate/LaravelSServiceProvider.php:
--------------------------------------------------------------------------------
1 | publishes([
12 | __DIR__ . '/../../config/laravels.php' => base_path('config/laravels.php'),
13 | ]);
14 | }
15 |
16 | public function register()
17 | {
18 | $this->mergeConfigFrom(
19 | __DIR__ . '/../../config/laravels.php', 'laravels'
20 | );
21 |
22 | $this->commands([
23 | LaravelSCommand::class,
24 | ListPropertiesCommand::class,
25 | ]);
26 | }
27 | }
--------------------------------------------------------------------------------
/src/Illuminate/LaravelScheduleJob.php:
--------------------------------------------------------------------------------
1 | > /dev/null 2>&1 &');
25 | }
26 | }
--------------------------------------------------------------------------------
/src/Illuminate/LaravelTrait.php:
--------------------------------------------------------------------------------
1 | prepareLaravel();
13 | $laravel->bindSwoole($swoole);
14 | return $laravel;
15 | }
16 | }
--------------------------------------------------------------------------------
/src/Illuminate/ListPropertiesCommand.php:
--------------------------------------------------------------------------------
1 | handle();
24 | }
25 |
26 | /**
27 | * @throws \ReflectionException
28 | */
29 | public function handle()
30 | {
31 | $this->outputTable();
32 | }
33 |
34 | /**
35 | * Output all properties of all controllers as table.
36 | *
37 | * @throws \ReflectionException
38 | */
39 | private function outputTable()
40 | {
41 | $allProperties = $this->allControllerProperties();
42 | foreach ($allProperties as $controller => $properties) {
43 | if (empty($properties)) {
44 | continue;
45 | }
46 |
47 | $this->table(
48 | ['Controller', 'Property', 'Property Modifier'],
49 | $properties
50 | );
51 | }
52 | }
53 |
54 | /**
55 | * Get all properties of all controllers.
56 | *
57 | * @return array
58 | * @throws \ReflectionException
59 | */
60 | private function allControllerProperties()
61 | {
62 | $controllers = $this->allControllers();
63 | array_walk($controllers, function (&$properties, $controller) {
64 | $properties = [];
65 | // Get parent's properties
66 | $parent = get_parent_class($controller);
67 | if ($parent) {
68 | $reflectParentController = new \ReflectionClass($parent);
69 | $parentProperties = $reflectParentController->getProperties();
70 | foreach ($parentProperties as $property) {
71 | $properties[$property->getName()] = [
72 | $controller => $controller,
73 | 'Property' => $property->getName(),
74 | 'Property Modifier' => $this->resolveModifiers($property),
75 | ];
76 | }
77 | }
78 |
79 | // Get sub controller's properties, override the parent properties.
80 | $reflectController = new \ReflectionClass($controller);
81 | $subProperties = $reflectController->getProperties();
82 | foreach ($subProperties as $property) {
83 | $properties[$property->getName()] = [
84 | $controller => $controller,
85 | 'Property' => $property->getName(),
86 | 'Property Modifier' => $this->resolveModifiers($property),
87 | ];
88 | }
89 | });
90 | return $controllers;
91 | }
92 |
93 | /**
94 | * Get all controllers
95 | *
96 | * @return array
97 | * @throws \ReflectionException
98 | */
99 | private function allControllers()
100 | {
101 | $controllers = [];
102 | $router = isset(app()->router) ? app()->router : (app()->offsetExists('router') ? app('router') : app());
103 | $routes = $router->getRoutes();
104 | if (is_array($routes)) {
105 | $uses = array_column(array_column($routes, 'action'), 'uses');
106 | } else {
107 | $property = new \ReflectionProperty(get_class($routes), 'actionList');
108 | $property->setAccessible(true);
109 | $uses = array_keys($property->getValue($routes));
110 | }
111 |
112 | foreach ($uses as $use) {
113 | list($controller,) = explode('@', $use);
114 | $controllers[$controller] = $controller;
115 | }
116 | return $controllers;
117 | }
118 |
119 | /**
120 | * Resolve modifiers from \ReflectionProperty
121 | *
122 | * @param \ReflectionProperty $property
123 | * @return string
124 | */
125 | private function resolveModifiers(\ReflectionProperty $property)
126 | {
127 | if ($property->isPublic()) {
128 | $modifier = 'public';
129 | } elseif ($property->isProtected()) {
130 | $modifier = 'protected';
131 | } elseif ($property->isPrivate()) {
132 | $modifier = 'private';
133 | } else {
134 | $modifier = ' ';
135 | }
136 | if ($property->isStatic()) {
137 | $modifier .= ' static';
138 | }
139 | return $modifier;
140 | }
141 | }
--------------------------------------------------------------------------------
/src/Illuminate/LogTrait.php:
--------------------------------------------------------------------------------
1 | log(
13 | sprintf(
14 | 'Uncaught exception \'%s\': [%d]%s called in %s:%d%s%s',
15 | get_class($e),
16 | $e->getCode(),
17 | $e->getMessage(),
18 | $e->getFile(),
19 | $e->getLine(),
20 | PHP_EOL,
21 | $e->getTraceAsString()
22 | ),
23 | 'ERROR'
24 | );
25 | }
26 |
27 | public function log($msg, $type = 'INFO')
28 | {
29 | $outputStyle = LaravelS::getOutputStyle();
30 | $msg = sprintf('[%s] [%s] %s', date('Y-m-d H:i:s'), $type, $msg);
31 | if ($outputStyle) {
32 | switch (strtoupper($type)) {
33 | case 'INFO':
34 | $outputStyle->writeln("{$msg}");
35 | break;
36 | case 'WARNING':
37 | if (!$outputStyle->getFormatter()->hasStyle('warning')) {
38 | $style = new OutputFormatterStyle('yellow');
39 | $outputStyle->getFormatter()->setStyle('warning', $style);
40 | }
41 | $outputStyle->writeln("{$msg}");
42 | break;
43 | case 'ERROR':
44 | $outputStyle->writeln("{$msg}");
45 | break;
46 | case 'TRACE':
47 | default:
48 | $outputStyle->writeln($msg);
49 | break;
50 | }
51 | } else {
52 | echo $msg, PHP_EOL;
53 | }
54 | }
55 |
56 | public function trace($msg)
57 | {
58 | $this->log($msg, 'TRACE');
59 | }
60 |
61 | public function info($msg)
62 | {
63 | $this->log($msg, 'INFO');
64 | }
65 |
66 | public function warning($msg)
67 | {
68 | $this->log($msg, 'WARNING');
69 | }
70 |
71 | public function error($msg)
72 | {
73 | $this->log($msg, 'ERROR');
74 | }
75 |
76 | public function callWithCatchException(callable $callback, array $args = [], $tries = 1)
77 | {
78 | $try = 0;
79 | do {
80 | $try++;
81 | try {
82 | return call_user_func_array($callback, $args);
83 | } catch (\Exception $e) {
84 | $this->logException($e);
85 | }
86 | } while ($try < $tries);
87 | return null;
88 | }
89 | }
--------------------------------------------------------------------------------
/src/Illuminate/ReflectionApp.php:
--------------------------------------------------------------------------------
1 | app = $app;
28 |
29 | $this->reflectionApp = new \ReflectionObject($app);
30 | }
31 |
32 | /**
33 | * Get all bindings from application container.
34 | *
35 | * @return array
36 | * @throws \ReflectionException
37 | */
38 | public function instances()
39 | {
40 | $instances = $this->reflectionApp->getProperty('instances');
41 | $instances->setAccessible(true);
42 | $instances = array_merge($this->app->getBindings(), $instances->getValue($this->app));
43 |
44 | return $instances;
45 | }
46 |
47 | /**
48 | * Call terminable middleware of Lumen.
49 | *
50 | * @param Response $response
51 | * @throws \ReflectionException
52 | */
53 | public function callTerminableMiddleware(Response $response)
54 | {
55 | $middleware = $this->reflectionApp->getProperty('middleware');
56 | $middleware->setAccessible(true);
57 |
58 | if (!empty($middleware->getValue($this->app))) {
59 | $callTerminableMiddleware = $this->reflectionApp->getMethod('callTerminableMiddleware');
60 | $callTerminableMiddleware->setAccessible(true);
61 | $callTerminableMiddleware->invoke($this->app, $response);
62 | }
63 | }
64 |
65 | /**
66 | * The parameter count of 'register' method in app container.
67 | *
68 | * @return int
69 | * @throws \ReflectionException
70 | */
71 | public function registerMethodParameterCount()
72 | {
73 | return $this->reflectionApp->getMethod('register')->getNumberOfParameters();
74 | }
75 |
76 | /**
77 | * Get 'loadedProviders' of application container.
78 | *
79 | * @return array
80 | * @throws \ReflectionException
81 | */
82 | public function loadedProviders()
83 | {
84 | $loadedProviders = $this->reflectLoadedProviders();
85 | return $loadedProviders->getValue($this->app);
86 | }
87 |
88 | /**
89 | * Set 'loadedProviders' of application container.
90 | *
91 | * @param array $loadedProviders
92 | * @throws \ReflectionException
93 | */
94 | public function setLoadedProviders(array $loadedProviders)
95 | {
96 | $reflectLoadedProviders = $this->reflectLoadedProviders();
97 | $reflectLoadedProviders->setValue($this->app, $loadedProviders);
98 | }
99 |
100 | /**
101 | * Get the reflect loadedProviders of application container.
102 | *
103 | * @return \ReflectionProperty
104 | * @throws \ReflectionException
105 | */
106 | protected function reflectLoadedProviders()
107 | {
108 | $loadedProviders = $this->reflectionApp->getProperty('loadedProviders');
109 | $loadedProviders->setAccessible(true);
110 | return $loadedProviders;
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/src/LaravelS.php:
--------------------------------------------------------------------------------
1 | Laravel Request
33 | * Laravel Request => Laravel handle => Laravel Response
34 | * Laravel Response => Swoole Response
35 | */
36 | class LaravelS extends Server
37 | {
38 | /**
39 | * Fix conflicts of traits
40 | */
41 | use InotifyTrait, LaravelTrait, LogTrait, ProcessTitleTrait, TimerTrait, CustomProcessTrait;
42 |
43 | /**@var array */
44 | protected array $laravelConf;
45 |
46 | /**@var Laravel */
47 | protected Laravel $laravel;
48 |
49 | /**@var Process[] */
50 | protected static array $customProcesses = [];
51 |
52 | public function __construct(array $svrConf, array $laravelConf)
53 | {
54 | parent::__construct($svrConf);
55 | $this->laravelConf = $laravelConf;
56 |
57 | $timerConf = $this->conf['timer'] ?? [];
58 | $timerConf['process_prefix'] = $svrConf['process_prefix'];
59 | $this->addTimerProcess($this->swoole, $timerConf, $this->laravelConf);
60 |
61 | $inotifyConf = $this->conf['inotify_reload'] ?? [];
62 | if (!isset($inotifyConf['watch_path'])) {
63 | $inotifyConf['watch_path'] = $this->laravelConf['root_path'];
64 | }
65 | $inotifyConf['process_prefix'] = $svrConf['process_prefix'];
66 | $this->addInotifyProcess($this->swoole, $inotifyConf, $this->laravelConf);
67 |
68 | $processes = $this->conf['processes'] ?? [];
69 | static::$customProcesses = $this->addCustomProcesses($this->swoole, $svrConf['process_prefix'], $processes, $this->laravelConf);
70 |
71 | // Fire ServerStart event
72 | if (isset($this->conf['event_handlers']['ServerStart'])) {
73 | Laravel::autoload($this->laravelConf['root_path']);
74 | $this->fireEvent('ServerStart', ServerStartInterface::class, [$this->swoole]);
75 | }
76 | }
77 |
78 | public static function getCustomProcesses(): array
79 | {
80 | return static::$customProcesses;
81 | }
82 |
83 | protected function beforeWebSocketHandShake(SwooleRequest $request)
84 | {
85 | // Start Laravel's lifetime, then support session ...middleware.
86 | $laravelRequest = $this->convertRequest($this->laravel, $request);
87 | $this->laravel->bindRequest($laravelRequest);
88 | $this->laravel->fireEvent('laravels.received_request', [$laravelRequest]);
89 | $this->laravel->cleanProviders();
90 | $laravelResponse = $this->laravel->handleDynamic($laravelRequest);
91 | $this->laravel->fireEvent('laravels.generated_response', [$laravelRequest, $laravelResponse]);
92 | }
93 |
94 | protected function afterWebSocketOpen(SwooleRequest $request)
95 | {
96 | // End Laravel's lifetime.
97 | $this->laravel->saveSession();
98 | $this->laravel->clean();
99 | }
100 |
101 | protected function triggerWebSocketEvent($event, array $params)
102 | {
103 | if ($event === 'onHandShake') {
104 | $this->beforeWebSocketHandShake($params[0]);
105 | if (!empty($this->conf['server'])) {
106 | $params[1]->header('Server', $this->conf['server']);
107 | }
108 | }
109 |
110 | parent::triggerWebSocketEvent($event, $params);
111 |
112 | switch ($event) {
113 | case 'onHandShake':
114 | if (isset($params[1]->header['Sec-Websocket-Accept'])) {
115 | // Successful handshake
116 | parent::triggerWebSocketEvent('onOpen', [$this->swoole, $params[0]]);
117 | }
118 | $this->afterWebSocketOpen($params[0]);
119 | break;
120 | case 'onOpen':
121 | $this->afterWebSocketOpen($params[1]);
122 | break;
123 | }
124 | }
125 |
126 | protected function triggerPortEvent(Port $port, $handlerClass, $event, array $params)
127 | {
128 | switch ($event) {
129 | case 'onHandShake':
130 | $this->beforeWebSocketHandShake($params[0]);
131 | case 'onRequest':
132 | if (!empty($this->conf['server'])) {
133 | $params[1]->header('Server', $this->conf['server']);
134 | }
135 | break;
136 | }
137 |
138 | parent::triggerPortEvent($port, $handlerClass, $event, $params);
139 |
140 | switch ($event) {
141 | case 'onHandShake':
142 | if (isset($params[1]->header['Sec-Websocket-Accept'])) {
143 | // Successful handshake
144 | parent::triggerPortEvent($port, $handlerClass, 'onOpen', [$this->swoole, $params[0]]);
145 | }
146 | $this->afterWebSocketOpen($params[0]);
147 | break;
148 | case 'onOpen':
149 | $this->afterWebSocketOpen($params[1]);
150 | break;
151 | }
152 | }
153 |
154 | public function onShutdown(HttpServer $server)
155 | {
156 | parent::onShutdown($server);
157 |
158 | // Fire ServerStop event
159 | if (isset($this->conf['event_handlers']['ServerStop'])) {
160 | $this->laravel = $this->initLaravel($this->laravelConf, $this->swoole);
161 | $this->fireEvent('ServerStop', ServerStopInterface::class, [$server]);
162 | }
163 | }
164 |
165 | public function onWorkerStart(HttpServer $server, $workerId)
166 | {
167 | parent::onWorkerStart($server, $workerId);
168 |
169 | // To implement gracefully reload
170 | // Delay to create Laravel
171 | // Delay to include Laravel's autoload.php
172 | $this->laravel = $this->initLaravel($this->laravelConf, $this->swoole);
173 |
174 | // Fire WorkerStart event
175 | $this->fireEvent('WorkerStart', WorkerStartInterface::class, func_get_args());
176 | }
177 |
178 | public function onWorkerStop(HttpServer $server, $workerId)
179 | {
180 | parent::onWorkerStop($server, $workerId);
181 |
182 | // Fire WorkerStop event
183 | $this->fireEvent('WorkerStop', WorkerStopInterface::class, func_get_args());
184 | }
185 |
186 | public function onWorkerError(HttpServer $server, $workerId, $workerPId, $exitCode, $signal)
187 | {
188 | parent::onWorkerError($server, $workerId, $workerPId, $exitCode, $signal);
189 |
190 | Laravel::autoload($this->laravelConf['root_path']);
191 |
192 | // Fire WorkerError event
193 | $this->fireEvent('WorkerError', WorkerErrorInterface::class, func_get_args());
194 | }
195 |
196 | protected function convertRequest(Laravel $laravel, SwooleRequest $request)
197 | {
198 | $rawGlobals = $laravel->getRawGlobals();
199 | return (new Request($request))->toIlluminateRequest($rawGlobals['_SERVER'], $rawGlobals['_ENV']);
200 | }
201 |
202 | public function onRequest(SwooleRequest $swooleRequest, SwooleResponse $swooleResponse)
203 | {
204 | try {
205 | $laravelRequest = $this->convertRequest($this->laravel, $swooleRequest);
206 | $this->laravel->bindRequest($laravelRequest);
207 | $this->laravel->fireEvent('laravels.received_request', [$laravelRequest]);
208 | $handleStaticSuccess = false;
209 | if ($this->conf['handle_static']) {
210 | // For Swoole < 1.9.17
211 | $handleStaticSuccess = $this->handleStaticResource($this->laravel, $laravelRequest, $swooleResponse);
212 | }
213 | if (!$handleStaticSuccess) {
214 | $this->handleDynamicResource($this->laravel, $laravelRequest, $swooleResponse);
215 | }
216 | } catch (\Exception $e) {
217 | $this->handleException($e, $swooleResponse);
218 | }
219 | }
220 |
221 | /**
222 | * @param \Exception $e
223 | * @param SwooleResponse $response
224 | */
225 | protected function handleException($e, SwooleResponse $response)
226 | {
227 | $error = sprintf(
228 | 'onRequest: Uncaught exception "%s"([%d]%s) at %s:%s, %s%s',
229 | get_class($e),
230 | $e->getCode(),
231 | $e->getMessage(),
232 | $e->getFile(),
233 | $e->getLine(),
234 | PHP_EOL,
235 | $e->getTraceAsString()
236 | );
237 | $this->error($error);
238 | try {
239 | $response->status(500);
240 | $response->end('Oops! An unexpected error occurred');
241 | } catch (\Exception $e) {
242 | $this->logException($e);
243 | }
244 | }
245 |
246 | protected function handleStaticResource(Laravel $laravel, IlluminateRequest $laravelRequest, SwooleResponse $swooleResponse)
247 | {
248 | $laravelResponse = $laravel->handleStatic($laravelRequest);
249 | if ($laravelResponse === false) {
250 | return false;
251 | }
252 | if (!empty($this->conf['server'])) {
253 | $laravelResponse->headers->set('Server', $this->conf['server'], true);
254 | }
255 | $laravel->fireEvent('laravels.generated_response', [$laravelRequest, $laravelResponse]);
256 | $response = new StaticResponse($swooleResponse, $laravelResponse);
257 | $response->setChunkLimit($this->conf['swoole']['buffer_output_size']);
258 | $response->send($this->conf['enable_gzip']);
259 | return true;
260 | }
261 |
262 | protected function handleDynamicResource(Laravel $laravel, IlluminateRequest $laravelRequest, SwooleResponse $swooleResponse)
263 | {
264 | $laravel->cleanProviders();
265 | $laravelResponse = $laravel->handleDynamic($laravelRequest);
266 | if (!empty($this->conf['server'])) {
267 | $laravelResponse->headers->set('Server', $this->conf['server'], true);
268 | }
269 | $laravel->fireEvent('laravels.generated_response', [$laravelRequest, $laravelResponse]);
270 | if ($laravelResponse instanceof BinaryFileResponse) {
271 | $response = new StaticResponse($swooleResponse, $laravelResponse);
272 | } else {
273 | $response = new DynamicResponse($swooleResponse, $laravelResponse);
274 | }
275 | $response->setChunkLimit($this->conf['swoole']['buffer_output_size']);
276 | $response->send($this->conf['enable_gzip']);
277 | $laravel->clean();
278 | return true;
279 | }
280 |
281 | /**@var OutputStyle */
282 | protected static $outputStyle;
283 |
284 | public static function setOutputStyle(OutputStyle $outputStyle)
285 | {
286 | static::$outputStyle = $outputStyle;
287 | }
288 |
289 | public static function getOutputStyle()
290 | {
291 | return static::$outputStyle;
292 | }
293 | }
294 |
--------------------------------------------------------------------------------
/src/Swoole/Coroutine/Context.php:
--------------------------------------------------------------------------------
1 | 0) {
27 | self::$box[$cid][$key] = $item;
28 | }
29 | }
30 |
31 | public static function delete($key = null)
32 | {
33 | $cid = Coroutine::getCid();
34 | if ($cid > 0) {
35 | if ($key) {
36 | unset(self::$box[$cid][$key]);
37 | } else {
38 | unset(self::$box[$cid]);
39 | }
40 | }
41 | }
42 |
43 | public static function inCoroutine()
44 | {
45 | return class_exists('Swoole\Coroutine', false) && Coroutine::getCid() > 0;
46 | }
47 | }
--------------------------------------------------------------------------------
/src/Swoole/DynamicResponse.php:
--------------------------------------------------------------------------------
1 | swooleResponse->gzip(2);
16 | } else {
17 | throw new \RuntimeException('Http GZIP requires library "zlib", use "php --ri zlib" to check.');
18 | }
19 | }
20 |
21 | public function sendContent()
22 | {
23 | if ($this->laravelResponse instanceof StreamedResponse) {
24 | ob_start();
25 | $this->laravelResponse = $this->laravelResponse->sendContent();
26 | $content = ob_get_clean();
27 | } else {
28 | $content = $this->laravelResponse->getContent();
29 | }
30 |
31 | $len = strlen($content);
32 | if ($len === 0) {
33 | $this->swooleResponse->end();
34 | return;
35 | }
36 |
37 | if ($len > $this->chunkLimit) {
38 | for ($offset = 0, $limit = (int)(0.6 * $this->chunkLimit); $offset < $len; $offset += $limit) {
39 | $chunk = substr($content, $offset, $limit);
40 | $this->swooleResponse->write($chunk);
41 | }
42 | $this->swooleResponse->end();
43 | } else {
44 | $this->swooleResponse->end($content);
45 | }
46 | }
47 | }
--------------------------------------------------------------------------------
/src/Swoole/Events/ServerStartInterface.php:
--------------------------------------------------------------------------------
1 | fd = inotify_init();
23 | $this->watchPath = $watchPath;
24 | $this->watchMask = $watchMask;
25 | $this->watchHandler = $watchHandler;
26 | }
27 |
28 | public function addFileType($type)
29 | {
30 | $type = '.' . trim($type, '.');
31 | $this->fileTypes[$type] = true;
32 | }
33 |
34 | public function addFileTypes(array $types)
35 | {
36 | foreach ($types as $type) {
37 | $this->addFileType($type);
38 | }
39 | }
40 |
41 | public function addExcludedDir($dir)
42 | {
43 | $dir = realpath($dir);
44 | $this->excludedDirs[$dir] = $dir;
45 | }
46 |
47 | public function addExcludedDirs(array $dirs)
48 | {
49 | foreach ($dirs as $dir) {
50 | $this->addExcludedDir($dir);
51 | }
52 | }
53 |
54 | public function isExcluded($path)
55 | {
56 | foreach ($this->excludedDirs as $excludedDir) {
57 | if ($excludedDir === $path || strpos($path, $excludedDir . '/') === 0) {
58 | return true;
59 | }
60 | }
61 | return false;
62 | }
63 |
64 | public function watch()
65 | {
66 | $this->_watch($this->watchPath);
67 | }
68 |
69 | protected function _watch($path)
70 | {
71 | if ($this->isExcluded($path)) {
72 | return false;
73 | }
74 | $wd = inotify_add_watch($this->fd, $path, $this->watchMask);
75 | if ($wd === false) {
76 | return false;
77 | }
78 | $this->bind($wd, $path);
79 |
80 | if (is_dir($path)) {
81 | $wd = inotify_add_watch($this->fd, $path, $this->watchMask);
82 | if ($wd === false) {
83 | return false;
84 | }
85 | $this->bind($wd, $path);
86 | $files = scandir($path);
87 | foreach ($files as $file) {
88 | if ($file === '.' || $file === '..' || $this->isExcluded($file)) {
89 | continue;
90 | }
91 | $file = $path . DIRECTORY_SEPARATOR . $file;
92 | if (is_dir($file)) {
93 | $this->_watch($file);
94 | }
95 | }
96 | }
97 | return true;
98 | }
99 |
100 | protected function clearWatch()
101 | {
102 | foreach ($this->wdPath as $wd => $path) {
103 | @inotify_rm_watch($this->fd, $wd);
104 | }
105 | $this->wdPath = [];
106 | $this->pathWd = [];
107 | }
108 |
109 | protected function bind($wd, $path)
110 | {
111 | $this->pathWd[$path] = $wd;
112 | $this->wdPath[$wd] = $path;
113 | }
114 |
115 | protected function unbind($wd, $path = null)
116 | {
117 | unset($this->wdPath[$wd]);
118 | if ($path !== null) {
119 | unset($this->pathWd[$path]);
120 | }
121 | }
122 |
123 | public function start()
124 | {
125 | Event::add($this->fd, function ($fp) {
126 | $events = inotify_read($fp);
127 | foreach ($events as $event) {
128 | if ($event['mask'] == IN_IGNORED) {
129 | continue;
130 | }
131 |
132 | $fileType = strchr($event['name'], '.');
133 | if (!isset($this->fileTypes[$fileType])) {
134 | continue;
135 | }
136 |
137 | if ($this->doing) {
138 | continue;
139 | }
140 |
141 | Timer::after(100, function () use ($event) {
142 | call_user_func_array($this->watchHandler, [$event]);
143 | $this->doing = false;
144 | });
145 | $this->doing = true;
146 | break;
147 | }
148 | });
149 | Event::wait();
150 | }
151 |
152 | public function stop()
153 | {
154 | Event::del($this->fd);
155 | fclose($this->fd);
156 | }
157 |
158 | public function getWatchedFileCount()
159 | {
160 | return count($this->wdPath);
161 | }
162 |
163 | public function __destruct()
164 | {
165 | $this->stop();
166 | }
167 | }
168 |
--------------------------------------------------------------------------------
/src/Swoole/InotifyTrait.php:
--------------------------------------------------------------------------------
1 | warning('Require extension inotify');
19 | return false;
20 | }
21 |
22 | $fileTypes = isset($config['file_types']) ? (array)$config['file_types'] : [];
23 | if (empty($fileTypes)) {
24 | $this->warning('No file types to watch by inotify');
25 | return false;
26 | }
27 |
28 | $callback = function () use ($config, $laravelConf) {
29 | $log = !empty($config['log']);
30 | $this->setProcessTitle(sprintf('%s laravels: inotify process', $config['process_prefix']));
31 | $inotify = new Inotify($config['watch_path'], IN_CREATE | IN_DELETE | IN_MODIFY | IN_MOVE,
32 | function ($event) use ($log, $laravelConf) {
33 | Portal::runLaravelSCommand($laravelConf['root_path'], 'reload');
34 | if ($log) {
35 | $action = 'file:';
36 | switch ($event['mask']) {
37 | case IN_CREATE:
38 | $action = 'create';
39 | break;
40 | case IN_DELETE:
41 | $action = 'delete';
42 | break;
43 | case IN_MODIFY:
44 | $action = 'modify';
45 | break;
46 | case IN_MOVE:
47 | $action = 'move';
48 | break;
49 | }
50 | $this->info(sprintf('reloaded by inotify, reason: %s %s', $action, $event['name']));
51 | }
52 | });
53 | $inotify->addFileTypes($config['file_types']);
54 | if (empty($config['excluded_dirs'])) {
55 | $config['excluded_dirs'] = [];
56 | }
57 | $inotify->addExcludedDirs($config['excluded_dirs']);
58 | $inotify->watch();
59 | if ($log) {
60 | $this->info(sprintf('[Inotify] watched files: %d; file types: %s; excluded directories: %s',
61 | $inotify->getWatchedFileCount(),
62 | implode(',', $config['file_types']),
63 | implode(',', $config['excluded_dirs'])
64 | )
65 | );
66 | }
67 | $inotify->start();
68 | };
69 |
70 | $process = new Process($callback, false, 0);
71 | $swoole->addProcess($process);
72 | return $process;
73 | }
74 | }
--------------------------------------------------------------------------------
/src/Swoole/Process/CustomProcessInterface.php:
--------------------------------------------------------------------------------
1 | setting['pid_file']) . '/' . $this->customProcessPidFile;
15 | if (file_exists($pidfile)) {
16 | unlink($pidfile);
17 | }
18 |
19 | /**@var []Process $processList */
20 | $processList = [];
21 | foreach ($processes as $name => $item) {
22 | if (empty($item['class'])) {
23 | throw new \InvalidArgumentException(sprintf('The class of process %s must be specified', $name));
24 | }
25 | if (isset($item['enable']) && !$item['enable']) {
26 | continue;
27 | }
28 | $processClass = $item['class'];
29 | $restartInterval = isset($item['restart_interval']) ? (int)$item['restart_interval'] : 5;
30 | $callback = function (Process $worker) use ($pidfile, $swoole, $processPrefix, $processClass, $restartInterval, $name, $laravelConfig) {
31 | file_put_contents($pidfile, $worker->pid . "\n", FILE_APPEND | LOCK_EX);
32 | $this->initLaravel($laravelConfig, $swoole);
33 | if (!isset(class_implements($processClass)[CustomProcessInterface::class])) {
34 | throw new \InvalidArgumentException(
35 | sprintf(
36 | '%s must implement the interface %s',
37 | $processClass,
38 | CustomProcessInterface::class
39 | )
40 | );
41 | }
42 | /**@var CustomProcessInterface $processClass */
43 | $this->setProcessTitle(sprintf('%s laravels: %s process', $processPrefix, $name));
44 |
45 | Process::signal(SIGUSR1, function ($signo) use ($name, $processClass, $worker, $pidfile, $swoole) {
46 | $this->info(sprintf('Reloading %s process[PID=%d].', $name, $worker->pid));
47 | $processClass::onReload($swoole, $worker);
48 | });
49 |
50 | if (method_exists($processClass, 'onStop')) {
51 | Process::signal(SIGTERM, function ($signo) use ($name, $processClass, $worker, $pidfile, $swoole) {
52 | $this->info(sprintf('Stopping %s process[PID=%d].', $name, $worker->pid));
53 | $processClass::onStop($swoole, $worker);
54 | });
55 | }
56 |
57 | if (class_exists('Swoole\Runtime')) {
58 | \Swoole\Runtime::enableCoroutine();
59 | }
60 |
61 | $this->callWithCatchException([$processClass, 'callback'], [$swoole, $worker]);
62 |
63 | // Avoid frequent process creation
64 | if (class_exists('Swoole\Coroutine')) {
65 | \Swoole\Coroutine::sleep($restartInterval);
66 | } else {
67 | sleep($restartInterval);
68 | }
69 | };
70 |
71 | if (isset($item['num']) && $item['num'] > 1) { // For multiple processes
72 | for ($i = 0; $i < $item['num']; $i++) {
73 | $process = $this->makeProcess($callback, $item);
74 | $swoole->addProcess($process);
75 | $processList[$name . $i] = $process;
76 | }
77 | } else { // For single process
78 | $process = $this->makeProcess($callback, $item);
79 | $swoole->addProcess($process);
80 | $processList[$name] = $process;
81 | }
82 | }
83 | return $processList;
84 | }
85 |
86 | /**
87 | * @param callable $callback
88 | * @param array $config
89 | * @return Process
90 | */
91 | public function makeProcess(callable $callback, array $config)
92 | {
93 | $redirect = isset($config['redirect']) ? $config['redirect'] : false;
94 | $pipe = isset($config['pipe']) ? $config['pipe'] : 0;
95 | $process = version_compare(SWOOLE_VERSION, '4.3.0', '>=')
96 | ? new Process($callback, $redirect, $pipe, class_exists('Swoole\Coroutine'))
97 | : new Process($callback, $redirect, $pipe);
98 | if (isset($config['queue'])) {
99 | if (empty($config['queue'])) {
100 | $process->useQueue();
101 | } else {
102 | $msgKey = isset($config['queue']['msg_key']) ? $config['queue']['msg_key'] : 0;
103 | $mode = isset($config['queue']['mode']) ? $config['queue']['mode'] : 2;
104 | $capacity = isset($config['queue']['capacity']) ? $config['queue']['capacity'] : -1;
105 | $process->useQueue($msgKey, $mode, $capacity);
106 | }
107 | }
108 |
109 | return $process;
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/src/Swoole/Process/ProcessTitleTrait.php:
--------------------------------------------------------------------------------
1 | swooleRequest = $request;
16 | }
17 |
18 | /**
19 | * Convert SwooleRequest to IlluminateRequest
20 | * @param array $rawServer
21 | * @param array $rawEnv
22 | * @return IlluminateRequest
23 | */
24 | public function toIlluminateRequest(array $rawServer = [], array $rawEnv = [])
25 | {
26 | $__GET = isset($this->swooleRequest->get) ? $this->swooleRequest->get : [];
27 | $__POST = isset($this->swooleRequest->post) ? $this->swooleRequest->post : [];
28 | $__COOKIE = isset($this->swooleRequest->cookie) ? $this->swooleRequest->cookie : [];
29 | $server = isset($this->swooleRequest->server) ? $this->swooleRequest->server : [];
30 | $headers = isset($this->swooleRequest->header) ? $this->swooleRequest->header : [];
31 | $__FILES = isset($this->swooleRequest->files) ? $this->swooleRequest->files : [];
32 | $__CONTENT = empty($__FILES) ? $this->swooleRequest->rawContent() : ''; // Cannot call rawContent() to avoid double the file memory when uploading a file.
33 | $_REQUEST = [];
34 | $_SESSION = [];
35 |
36 | static $headerServerMapping = [
37 | 'x-real-ip' => 'REMOTE_ADDR',
38 | 'x-real-port' => 'REMOTE_PORT',
39 | 'server-protocol' => 'SERVER_PROTOCOL',
40 | 'server-name' => 'SERVER_NAME',
41 | 'server-addr' => 'SERVER_ADDR',
42 | 'server-port' => 'SERVER_PORT',
43 | 'scheme' => 'REQUEST_SCHEME',
44 | ];
45 |
46 | $_ENV = $rawEnv;
47 | $_SERVER = $rawServer;
48 | foreach ($headers as $key => $value) {
49 | // Fix client && server's info
50 | if (isset($headerServerMapping[$key])) {
51 | $server[$headerServerMapping[$key]] = $value;
52 | } else {
53 | $key = str_replace('-', '_', $key);
54 | $server['http_' . $key] = $value;
55 | }
56 | }
57 | $server = array_change_key_case($server, CASE_UPPER);
58 | $_SERVER = array_merge($_SERVER, $server);
59 | if (isset($_SERVER['REQUEST_SCHEME']) && $_SERVER['REQUEST_SCHEME'] === 'https') {
60 | $_SERVER['HTTPS'] = 'on';
61 | }
62 |
63 | // Fix REQUEST_URI with QUERY_STRING
64 | if (strpos($_SERVER['REQUEST_URI'], '?') === false
65 | && isset($_SERVER['QUERY_STRING'])
66 | && $_SERVER['QUERY_STRING'] !== ''
67 | ) {
68 | $_SERVER['REQUEST_URI'] .= '?' . $_SERVER['QUERY_STRING'];
69 | }
70 |
71 | // Fix argv & argc
72 | if (!isset($_SERVER['argv'])) {
73 | $_SERVER['argv'] = isset($GLOBALS['argv']) ? $GLOBALS['argv'] : [];
74 | $_SERVER['argc'] = isset($GLOBALS['argc']) ? $GLOBALS['argc'] : 0;
75 | }
76 |
77 | // Initialize laravel request
78 | IlluminateRequest::enableHttpMethodParameterOverride();
79 | $request = IlluminateRequest::createFromBase(new \Symfony\Component\HttpFoundation\Request($__GET, $__POST, [], $__COOKIE, $__FILES, $_SERVER, $__CONTENT));
80 |
81 | if (0 === strpos($request->headers->get('CONTENT_TYPE', ''), 'application/x-www-form-urlencoded')
82 | && in_array(strtoupper($request->server->get('REQUEST_METHOD', 'GET')), ['PUT', 'DELETE', 'PATCH'])
83 | ) {
84 | parse_str($request->getContent(), $data);
85 | $request->request = new ParameterBag($data);
86 | }
87 |
88 | return $request;
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/src/Swoole/Response.php:
--------------------------------------------------------------------------------
1 | swooleResponse = $swooleResponse;
20 | $this->laravelResponse = $laravelResponse;
21 | }
22 |
23 | public function setChunkLimit($chunkLimit)
24 | {
25 | $this->chunkLimit = $chunkLimit;
26 | }
27 |
28 | public function sendStatusCode()
29 | {
30 | $this->swooleResponse->status($this->laravelResponse->getStatusCode());
31 | }
32 |
33 | private function getHeaders()
34 | {
35 | if (method_exists($this->laravelResponse->headers, 'allPreserveCaseWithoutCookies')) {
36 | return $this->laravelResponse->headers->allPreserveCaseWithoutCookies();
37 | }
38 |
39 | return $this->laravelResponse->headers->allPreserveCase();
40 | }
41 |
42 | public function sendHeaders()
43 | {
44 | $headers = $this->getHeaders();
45 | $trailers = isset($headers['trailer']) ? $headers['trailer'] : [];
46 |
47 | foreach ($headers as $name => $values) {
48 | if (in_array($name, $trailers, true)) {
49 | continue;
50 | }
51 | if (version_compare(SWOOLE_VERSION, '4.6.0', '>=')) {
52 | $this->swooleResponse->header($name, $values);
53 | } else {
54 | foreach ($values as $value) {
55 | $this->swooleResponse->header($name, $value);
56 | }
57 | }
58 | }
59 | }
60 |
61 | public function sendTrailers()
62 | {
63 | $headers = $this->getHeaders();
64 | $trailers = isset($headers['trailer']) ? $headers['trailer'] : [];
65 |
66 | foreach ($headers as $name => $values) {
67 | if (!in_array($name, $trailers, true)) {
68 | continue;
69 | }
70 |
71 | foreach ($values as $value) {
72 | $this->swooleResponse->trailer($name, $value);
73 | }
74 | }
75 | }
76 |
77 | public function sendCookies()
78 | {
79 | $hasIsRaw = null;
80 | /**@var \Symfony\Component\HttpFoundation\Cookie[] $cookies */
81 | $cookies = $this->laravelResponse->headers->getCookies();
82 | foreach ($cookies as $cookie) {
83 | if ($hasIsRaw === null) {
84 | $hasIsRaw = method_exists($cookie, 'isRaw');
85 | }
86 | $setCookie = $hasIsRaw && $cookie->isRaw() ? 'rawcookie' : 'cookie';
87 | $this->swooleResponse->$setCookie(
88 | $cookie->getName(),
89 | $cookie->getValue(),
90 | $cookie->getExpiresTime(),
91 | $cookie->getPath(),
92 | $cookie->getDomain(),
93 | $cookie->isSecure(),
94 | $cookie->isHttpOnly()
95 | );
96 | }
97 | }
98 |
99 | public function send($gzip = false)
100 | {
101 | $this->sendStatusCode();
102 | $this->sendHeaders();
103 | $this->sendCookies();
104 | $this->sendTrailers();
105 | if ($gzip) {
106 | $this->gzip();
107 | }
108 | $this->sendContent();
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/src/Swoole/ResponseInterface.php:
--------------------------------------------------------------------------------
1 | swoolePort = $port;
14 | }
15 | }
--------------------------------------------------------------------------------
/src/Swoole/Socket/HttpInterface.php:
--------------------------------------------------------------------------------
1 | swoolePort = $port;
15 | }
16 |
17 | public function onConnect(Server $server, $fd, $reactorId)
18 | {
19 | }
20 |
21 | public function onClose(Server $server, $fd, $reactorId)
22 | {
23 | }
24 |
25 | public function onBufferFull(Server $server, $fd)
26 | {
27 | }
28 |
29 | public function onBufferEmpty(Server $server, $fd)
30 | {
31 | }
32 |
33 | abstract public function onReceive(Server $server, $fd, $reactorId, $data);
34 | }
--------------------------------------------------------------------------------
/src/Swoole/Socket/UdpInterface.php:
--------------------------------------------------------------------------------
1 | swoolePort = $port;
15 | }
16 |
17 | abstract public function onPacket(Server $server, $data, array $clientInfo);
18 | }
--------------------------------------------------------------------------------
/src/Swoole/Socket/WebSocket.php:
--------------------------------------------------------------------------------
1 | swoolePort = $port;
14 | }
15 | }
--------------------------------------------------------------------------------
/src/Swoole/Socket/WebSocketInterface.php:
--------------------------------------------------------------------------------
1 | laravelResponse->getFile();
22 | if (!$this->laravelResponse->headers->has('Content-Type')) {
23 | $this->swooleResponse->header('Content-Type', $file->getMimeType());
24 | }
25 | if ($this->laravelResponse->getStatusCode() == BinaryFileResponse::HTTP_NOT_MODIFIED) {
26 | $this->swooleResponse->end();
27 | return;
28 | }
29 |
30 | $path = $file->getPathname();
31 | $size = filesize($path);
32 | if ($size <= 0) {
33 | $this->swooleResponse->end();
34 | return;
35 | }
36 |
37 | // Support deleteFileAfterSend: https://github.com/symfony/http-foundation/blob/5.0/BinaryFileResponse.php#L305
38 | $reflection = new \ReflectionObject($this->laravelResponse);
39 | if ($reflection->hasProperty('deleteFileAfterSend')) {
40 | $deleteFileAfterSend = $reflection->getProperty('deleteFileAfterSend');
41 | $deleteFileAfterSend->setAccessible(true);
42 | $deleteFile = $deleteFileAfterSend->getValue($this->laravelResponse);
43 | } else {
44 | $deleteFile = false;
45 | }
46 |
47 | if ($deleteFile) {
48 | $fp = fopen($path, 'rb');
49 |
50 | for ($offset = 0, $limit = (int)(0.99 * $this->chunkLimit); $offset < $size; $offset += $limit) {
51 | fseek($fp, $offset, SEEK_SET);
52 | $chunk = fread($fp, $limit);
53 | $this->swooleResponse->write($chunk);
54 | }
55 | $this->swooleResponse->end();
56 |
57 | fclose($fp);
58 |
59 | if (file_exists($path)) {
60 | unlink($path);
61 | }
62 | } else {
63 | if (version_compare(SWOOLE_VERSION, '1.7.21', '<')) {
64 | throw new \RuntimeException('sendfile() require Swoole >= 1.7.21');
65 | }
66 | $this->swooleResponse->sendfile($path);
67 | }
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/Swoole/Task/BaseTask.php:
--------------------------------------------------------------------------------
1 | delay = (int)$delay;
32 | return $this;
33 | }
34 |
35 | /**
36 | * Return the delay time.
37 | * @return int
38 | */
39 | public function getDelay()
40 | {
41 | return $this->delay;
42 | }
43 |
44 | /**
45 | * Set the number of tries.
46 | * @param int $tries
47 | * @return $this
48 | */
49 | public function setTries($tries)
50 | {
51 | if ($tries < 1) {
52 | throw new \InvalidArgumentException('The number of attempts must be greater than or equal to 1');
53 | }
54 | $this->tries = (int)$tries;
55 | return $this;
56 | }
57 |
58 | /**
59 | * Get the number of tries.
60 | * @return int
61 | */
62 | public function getTries()
63 | {
64 | return $this->tries;
65 | }
66 |
67 | /**
68 | * Deliver a task
69 | * @param mixed $task The task object
70 | * @return bool
71 | */
72 | protected function task($task)
73 | {
74 | static $dispatch;
75 | if (!$dispatch) {
76 | $dispatch = static function ($task) {
77 | /**@var \Swoole\Http\Server $swoole */
78 | $swoole = app('swoole');
79 | // The worker_id of timer process is -1
80 | if ($swoole->worker_id === -1 || $swoole->taskworker) {
81 | $workerNum = isset($swoole->setting['worker_num']) ? $swoole->setting['worker_num'] : 0;
82 | $availableId = mt_rand(0, $workerNum - 1);
83 | return $swoole->sendMessage($task, $availableId);
84 | }
85 | $taskId = $swoole->task($task);
86 | return $taskId !== false;
87 | };
88 | }
89 |
90 | if ($this->delay > 0) {
91 | Timer::after($this->delay * 1000, $dispatch, $task);
92 | return true;
93 | }
94 |
95 | return $dispatch($task);
96 | }
97 | }
--------------------------------------------------------------------------------
/src/Swoole/Task/Event.php:
--------------------------------------------------------------------------------
1 | listeners;
24 | }
25 |
26 | /**
27 | * Trigger an event
28 | * @param Event $event
29 | * @return bool
30 | */
31 | public static function fire(self $event)
32 | {
33 | return $event->task($event);
34 | }
35 | }
--------------------------------------------------------------------------------
/src/Swoole/Task/Listener.php:
--------------------------------------------------------------------------------
1 | task($task);
25 | }
26 | }
--------------------------------------------------------------------------------
/src/Swoole/Timer/BackupCronJob.php:
--------------------------------------------------------------------------------
1 | interval = $config[0];
58 | }
59 | if (isset($config[1])) {
60 | $this->isImmediate = $config[1];
61 | }
62 | }
63 | }
64 |
65 | /**
66 | * @return int
67 | */
68 | public function interval()
69 | {
70 | return $this->interval;
71 | }
72 |
73 | /**
74 | * @return bool
75 | */
76 | public function isImmediate()
77 | {
78 | return $this->isImmediate;
79 | }
80 |
81 | public function setTimerId($timerId)
82 | {
83 | $this->timerId = $timerId;
84 | }
85 |
86 | public function stop()
87 | {
88 | if ($this->timerId && Timer::exists($this->timerId)) {
89 | Timer::clear($this->timerId);
90 | }
91 | }
92 |
93 | public static function getGlobalTimerCacheKey()
94 | {
95 | return 'laravels:timer:' . strtolower(self::$globalTimerLockKey);
96 | }
97 |
98 | public static function getGlobalTimerLock()
99 | {
100 | /**@var \Illuminate\Redis\RedisManager $redis */
101 | $redis = app('redis');
102 |
103 | $key = self::getGlobalTimerCacheKey();
104 | $value = self::getCurrentInstanceId();
105 | $expire = self::GLOBAL_TIMER_LOCK_SECONDS;
106 | $result = $redis->set($key, $value, 'ex', $expire, 'nx');
107 | // Compatible with Predis and PhpRedis
108 | return $result === true || ((string)$result === 'OK');
109 | }
110 |
111 | protected static function getCurrentInstanceId()
112 | {
113 | return sprintf('%s:%d', current(swoole_get_local_ip()) ?: gethostname(), config('laravels.listen_port'));
114 | }
115 |
116 | public static function isGlobalTimerAlive()
117 | {
118 | /**@var \Redis|\RedisCluster|\Predis\Client $redis */
119 | $redis = app('redis')->client(); // Fix: Redis exists() always returns false on cluster mode for some older versions of Laravel/Lumen, see https://github.com/illuminate/redis/commit/62ff6a06a9c91902d3baa7feda20bab5e807606f
120 | return (bool)$redis->exists(self::getGlobalTimerCacheKey());
121 | }
122 |
123 | public static function isCurrentTimerAlive()
124 | {
125 | /**@var \Illuminate\Redis\RedisManager $redis */
126 | $redis = app('redis');
127 | $key = self::getGlobalTimerCacheKey();
128 | $instanceId = $redis->get($key);
129 | return $instanceId === self::getCurrentInstanceId();
130 | }
131 |
132 | public static function renewGlobalTimerLock($expire)
133 | {
134 | /**@var \Illuminate\Redis\RedisManager $redis */
135 | $redis = app('redis');
136 | return (bool)$redis->expire(self::getGlobalTimerCacheKey(), $expire);
137 | }
138 |
139 | public static function setGlobalTimerLockKey($lockKey)
140 | {
141 | self::$globalTimerLockKey = $lockKey;
142 | }
143 |
144 | public static function checkSetEnable()
145 | {
146 | if (self::isGlobalTimerAlive()) {
147 | // Reset current timer to avoid repeated execution
148 | self::setEnable(self::isCurrentTimerAlive());
149 | } else {
150 | // Compete for timer lock
151 | self::setEnable(self::getGlobalTimerLock());
152 | }
153 | }
154 |
155 | public static function setEnable($enable)
156 | {
157 | self::$enable = (bool)$enable;
158 | }
159 |
160 | public static function isEnable()
161 | {
162 | return self::$enable;
163 | }
164 | }
--------------------------------------------------------------------------------
/src/Swoole/Timer/CronJobInterface.php:
--------------------------------------------------------------------------------
1 | setting['pid_file']) . '/' . $this->timerPidFile;
30 | file_put_contents($pidfile, $process->pid);
31 | $this->setProcessTitle(sprintf('%s laravels: timer process', $config['process_prefix']));
32 | $this->initLaravel($laravelConfig, $swoole);
33 |
34 | // Implement global timer by Cache lock.
35 | if (!empty($config['global_lock'])) {
36 | CronJob::setGlobalTimerLockKey($config['global_lock_key']);
37 | CronJob::checkSetEnable();
38 | }
39 |
40 | $timerIds = $this->registerTimers($config['jobs']);
41 |
42 | Process::signal(SIGUSR1, function ($signo) use ($config, $timerIds, $process) {
43 | foreach ($timerIds as $timerId) {
44 | if (Timer::exists($timerId)) {
45 | Timer::clear($timerId);
46 | }
47 | }
48 | Timer::after($config['max_wait_time'] * 1000, function () use ($process) {
49 | $process->exit(0);
50 | });
51 | });
52 | // For Swoole 4.6.x
53 | // Deprecated: Swoole\Event::rshutdown(): Event::wait() in shutdown function is deprecated in Unknown on line 0
54 | Event::wait();
55 | };
56 |
57 | $process = new Process($callback, false, 0);
58 | $swoole->addProcess($process);
59 | return $process;
60 | }
61 |
62 | public function registerTimers(array $jobs)
63 | {
64 | $timerIds = [];
65 | foreach ($jobs as $jobClass) {
66 | if (is_array($jobClass) && isset($jobClass[0])) {
67 | $job = new $jobClass[0](isset($jobClass[1]) ? $jobClass[1] : []);
68 | } else {
69 | $job = new $jobClass();
70 | }
71 | if (!($job instanceof CronJob)) {
72 | throw new \InvalidArgumentException(sprintf(
73 | '%s must extend the abstract class %s',
74 | get_class($job),
75 | CronJob::class
76 | )
77 | );
78 | }
79 | if (empty($job->interval())) {
80 | throw new \InvalidArgumentException(sprintf('The interval of %s cannot be empty', get_class($job)));
81 | }
82 | $runJob = function () use ($job) {
83 | $runCallback = function () use ($job) {
84 | $this->callWithCatchException(function () use ($job) {
85 | if (($job instanceof CheckGlobalTimerAliveCronJob) || $job::isEnable()) {
86 | $job->run();
87 | }
88 | });
89 | };
90 | class_exists('Swoole\Coroutine') ? \Swoole\Coroutine::create($runCallback) : $runCallback();
91 | };
92 |
93 | $timerId = Timer::tick($job->interval(), $runJob);
94 | $timerIds[] = $timerId;
95 | $job->setTimerId($timerId);
96 | if ($job->isImmediate()) {
97 | Timer::after(1, $runJob);
98 | }
99 | }
100 | return $timerIds;
101 | }
102 | }
--------------------------------------------------------------------------------
/src/Swoole/WebSocketHandlerInterface.php:
--------------------------------------------------------------------------------
1 | httpGet('http://httpbin.org/get', ['timeout' => 3]);
14 | $this->assertIsArray($response);
15 | $this->assertArrayHasKey('body', $response);
16 | $body = $response['body'];
17 | $json = json_decode($body, true);
18 | $this->assertIsArray($json);
19 | }
20 | }
--------------------------------------------------------------------------------
/tests/TestCase.php:
--------------------------------------------------------------------------------
1 | &1 | grep 'Requests per second' | awk '{print $4}')
13 | echo "TEST#${i}: ${qps:-"-"}"
14 | total=$(awk "BEGIN{print ${total}+${qps:-0}}")
15 | done
16 | echo "AVG QPS:" $(awk "BEGIN{printf \"%.3f\", ${total}/${rc}}")
17 |
--------------------------------------------------------------------------------