Add swoole support. (#19)

Co-authored-by: 何延龙 <hey.yanlong@gmail.com>
diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml
index b649079..597408b 100644
--- a/.github/workflows/rust.yml
+++ b/.github/workflows/rust.yml
@@ -19,6 +19,7 @@
   push:
     branches:
       - master
+      - debug
   pull_request:
     branches:
       - "**"
diff --git a/README.md b/README.md
index da1c3a0..e0c59ae 100644
--- a/README.md
+++ b/README.md
@@ -33,6 +33,14 @@
   * [ ] [php-rdkafka](https://github.com/arnaud-lb/php-rdkafka)
   * [ ] [predis](https://github.com/predis/predis)
 
+* Swoole Ecosystem
+  * [ ] [Coroutine\Http\Client](https://wiki.swoole.com/#/coroutine_client/http_client)
+  * [ ] [Coroutine\MySQL](https://wiki.swoole.com/#/coroutine_client/mysql)
+  * [ ] [Swoole\Coroutine\Http\Client](https://wiki.swoole.com/#/coroutine_client/http_client)
+  * [ ] [Coroutine\Redis](https://wiki.swoole.com/#/coroutine_client/redis)
+
+  *The components of the PHP-FPM ecosystem can also be used in Swoole.*
+
 ## Contact Us
 
 * Mail list: **dev@skywalking.apache.org**. Mail to `dev-subscribe@skywalking.apache.org`, follow the reply to subscribe the mail list.
diff --git a/docs/en/setup/service-agent/php-agent/Supported-list.md b/docs/en/setup/service-agent/php-agent/Supported-list.md
index fafae02..0804886 100644
--- a/docs/en/setup/service-agent/php-agent/Supported-list.md
+++ b/docs/en/setup/service-agent/php-agent/Supported-list.md
@@ -5,6 +5,7 @@
 ## Supported SAPI
 
 * PHP-FPM
+* CLI under [Swoole](https://www.swoole.com/)
 
 ## Support PHP extension
 
diff --git a/src/lib.rs b/src/lib.rs
index c08a17c..4ffa85a 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -93,14 +93,13 @@
     module.on_request_init(request::init);
     module.on_request_shutdown(request::shutdown);
 
-    // TODO Add swoole in future.
     // The function is used by swoole plugin, to surround the callback of on
     // request.
-    // module.add_function(
-    //     "skywalking_hack_swoole_on_request_please_do_not_use",
-    //     request::skywalking_hack_swoole_on_request,
-    //     vec![],
-    // );
+    module.add_function(
+        "skywalking_hack_swoole_on_request_please_do_not_use",
+        request::skywalking_hack_swoole_on_request,
+        vec![],
+    );
 
     module
 }
diff --git a/src/module.rs b/src/module.rs
index ea96f16..332e5a8 100644
--- a/src/module.rs
+++ b/src/module.rs
@@ -106,7 +106,6 @@
     }
 }
 
-#[allow(dead_code)]
 fn get_module_registry() -> &'static ZArr {
     unsafe { ZArr::from_ptr(&sys::module_registry) }
 }
@@ -122,10 +121,9 @@
         return true;
     }
 
-    // TODO Add swoole in future.
-    // if sapi == b"cli" && get_module_registry().exists("swoole") {
-    //     return true;
-    // }
+    if sapi == b"cli" && get_module_registry().exists("swoole") {
+        return true;
+    }
 
     false
 }
diff --git a/src/plugin/mod.rs b/src/plugin/mod.rs
index 1e8851d..cffafc9 100644
--- a/src/plugin/mod.rs
+++ b/src/plugin/mod.rs
@@ -25,9 +25,8 @@
     vec![
         Box::new(plugin_curl::CurlPlugin::default()),
         Box::new(plugin_pdo::PdoPlugin::default()),
-        // TODO Add swoole in future.
-        // Box::new(plugin_swoole::SwooleServerPlugin::default()),
-        // Box::new(plugin_swoole::SwooleHttpResponsePlugin::default()),
+        Box::new(plugin_swoole::SwooleServerPlugin::default()),
+        Box::new(plugin_swoole::SwooleHttpResponsePlugin::default()),
     ]
 });
 
diff --git a/src/request.rs b/src/request.rs
index 51274e1..c6b3222 100644
--- a/src/request.rs
+++ b/src/request.rs
@@ -130,7 +130,6 @@
 
 /// The function is used by swoole plugin, to surround the callback of on
 /// request.
-#[allow(dead_code)]
 pub fn skywalking_hack_swoole_on_request(args: &mut [ZVal]) {
     let f = ORI_SWOOLE_ON_REQUEST.load(Ordering::Relaxed);
     if f.is_null() {
@@ -161,7 +160,6 @@
     }
 }
 
-#[allow(dead_code)]
 fn request_init_for_swoole(request: &mut ZVal) -> anyhow::Result<()> {
     let request = request
         .as_mut_z_obj()
@@ -191,7 +189,6 @@
     create_request_context(Some(fd), header.as_deref(), &method, &uri)
 }
 
-#[allow(dead_code)]
 fn request_shutdown_for_swoole(response: &mut ZVal) -> anyhow::Result<()> {
     let response = response
         .as_mut_z_obj()
diff --git a/tests/common/mod.rs b/tests/common/mod.rs
index 6701063..7e9fe85 100644
--- a/tests/common/mod.rs
+++ b/tests/common/mod.rs
@@ -48,6 +48,9 @@
 pub const PROXY_SERVER_2_ADDRESS: &str = "127.0.0.1:9012";
 pub const FPM_SERVER_1_ADDRESS: &str = "127.0.0.1:9001";
 pub const FPM_SERVER_2_ADDRESS: &str = "127.0.0.1:9002";
+pub const SWOOLE_SERVER_1_ADDRESS: &str = "127.0.0.1:9501";
+#[allow(dead_code)]
+pub const SWOOLE_SERVER_2_ADDRESS: &str = "127.0.0.1:9502";
 pub const COLLECTOR_GRPC_ADDRESS: &str = "127.0.0.1:19876";
 pub const COLLECTOR_HTTP_ADDRESS: &str = "127.0.0.1:12800";
 
@@ -72,6 +75,8 @@
     http_server_2_handle: JoinHandle<()>,
     php_fpm_1_child: Child,
     php_fpm_2_child: Child,
+    php_swoole_1_child: Child,
+    php_swoole_2_child: Child,
 }
 
 pub async fn setup() -> Fixture {
@@ -87,6 +92,8 @@
         )),
         php_fpm_1_child: setup_php_fpm(1, FPM_SERVER_1_ADDRESS),
         php_fpm_2_child: setup_php_fpm(2, FPM_SERVER_2_ADDRESS),
+        php_swoole_1_child: setup_php_swoole(1),
+        php_swoole_2_child: setup_php_swoole(2),
     }
 }
 
@@ -97,6 +104,8 @@
     let results = join_all([
         kill_command(fixture.php_fpm_1_child),
         kill_command(fixture.php_fpm_2_child),
+        kill_command(fixture.php_swoole_1_child),
+        kill_command(fixture.php_swoole_2_child),
     ])
     .await;
     for result in results {
@@ -247,11 +256,11 @@
     let args = [
         &php_fpm,
         "-F",
-        "-n",
+        // "-n",
+        // "-c",
+        // "tests/conf/php.ini",
         "-y",
         &format!("tests/conf/php-fpm.{}.conf", index),
-        "-c",
-        "tests/conf/php.ini",
         "-d",
         &format!("extension=target/{}/libskywalking_agent{}", TARGET, EXT),
         "-d",
@@ -286,6 +295,49 @@
         .unwrap()
 }
 
+#[instrument]
+fn setup_php_swoole(index: usize) -> Child {
+    let php = env::var("PHP_BIN").unwrap_or_else(|_| "php".to_string());
+    let args = [
+        &php,
+        // "-n",
+        // "-c",
+        // "tests/conf/php.ini",
+        "-d",
+        &format!("extension=target/{}/libskywalking_agent{}", TARGET, EXT),
+        "-d",
+        "skywalking_agent.enable=On",
+        "-d",
+        &format!(
+            "skywalking_agent.service_name=skywalking-agent-test-{}-swoole",
+            index
+        ),
+        "-d",
+        &format!(
+            "skywalking_agent.server_addr=http://{}",
+            COLLECTOR_GRPC_ADDRESS
+        ),
+        "-d",
+        &format!("skywalking_agent.log_level={}", PROCESS_LOG_LEVEL),
+        "-d",
+        &format!(
+            "skywalking_agent.log_file=/tmp/swoole-skywalking-agent.{}.log",
+            index
+        ),
+        "-d",
+        "skywalking.worker_threads=3",
+        &format!("tests/php/swoole/main.{}.php", index),
+    ];
+    info!(cmd = args.join(" "), "start command");
+    Command::new(&args[0])
+        .args(&args[1..])
+        .stdin(Stdio::null())
+        .stdout(File::create("/tmp/swoole-skywalking-stdout.log").unwrap())
+        .stderr(File::create("/tmp/swoole-skywalking-stderr.log").unwrap())
+        .spawn()
+        .unwrap()
+}
+
 async fn kill_command(mut child: Child) -> io::Result<ExitStatus> {
     if let Some(id) = child.id() {
         unsafe {
diff --git a/tests/conf/php.ini b/tests/conf/php.ini
index 81aec1a..8e03302 100644
--- a/tests/conf/php.ini
+++ b/tests/conf/php.ini
@@ -59,44 +59,44 @@
 allow_url_include = Off
 default_socket_timeout = 60
 
-extension=mysqlnd.so
-zend_extension=opcache.so
-extension=pdo.so
-extension=xml.so
-extension=bcmath.so
-extension=calendar.so
-extension=ctype.so
-extension=curl.so
-extension=dom.so
-extension=exif.so
-extension=fileinfo.so
-extension=ftp.so
-extension=gd.so
-extension=gettext.so
-extension=iconv.so
-extension=intl.so
-extension=json.so
-extension=mbstring.so
-extension=mysqli.so
-extension=pdo_mysql.so
-extension=phar.so
-extension=posix.so
-extension=readline.so
-; extension=redis.so
-extension=shmop.so
-extension=simplexml.so
-extension=sockets.so
-extension=sysvmsg.so
-extension=sysvsem.so
-extension=sysvshm.so
-extension=swoole.so
-extension=tokenizer.so
-extension=wddx.so
-extension=xmlreader.so
-extension=xmlwriter.so
-extension=xsl.so
-extension=yaml.so
-extension=zip.so
+extension=mysqlnd
+zend_extension=opcache
+extension=pdo
+extension=xml
+extension=bcmath
+extension=calendar
+extension=ctype
+extension=curl
+extension=dom
+extension=exif
+extension=fileinfo
+extension=ftp
+extension=gd
+extension=gettext
+extension=iconv
+extension=intl
+extension=json
+extension=mbstring
+extension=mysqli
+extension=pdo_mysql
+extension=phar
+extension=posix
+extension=readline
+; extension=redis
+extension=shmop
+extension=simplexml
+extension=sockets
+extension=sysvmsg
+extension=sysvsem
+extension=sysvshm
+extension=swoole
+extension=tokenizer
+extension=wddx
+extension=xmlreader
+extension=xmlwriter
+extension=xsl
+extension=yaml
+extension=zip
 
 ;;;;;;;;;;;;;;;;;;;
 ; Module Settings ;
diff --git a/tests/data/expected_context.yaml b/tests/data/expected_context.yaml
index eaf5a98..2ca62ea 100644
--- a/tests/data/expected_context.yaml
+++ b/tests/data/expected_context.yaml
@@ -387,3 +387,110 @@
                   parentService: skywalking-agent-test-1,
                   traceId: "not null",
                 }
+  - serviceName: skywalking-agent-test-1-swoole
+    segmentSize: 2
+    segments:
+      - segmentId: "not null"
+        spans:
+          - operationName: GET:/
+            parentSpanId: -1
+            spanId: 0
+            spanLayer: Http
+            startTime: gt 0
+            endTime: gt 0
+            componentId: 8001
+            isError: false
+            spanType: Entry
+            peer: ""
+            skipAnalysis: false
+            tags:
+              - { key: url, value: / }
+              - { key: http.method, value: GET }
+              - { key: http.status_code, value: "200" }
+            refs:
+              - {
+                  parentEndpoint: /,
+                  networkAddress: "127.0.0.1:9501",
+                  refType: CrossProcess,
+                  parentSpanId: 1,
+                  parentTraceSegmentId: "not null",
+                  parentServiceInstance: "not null",
+                  parentService: skywalking-agent-test-1-swoole,
+                  traceId: "not null",
+                }
+      - segmentId: "not null"
+        spans:
+          - operationName: /
+            parentSpanId: 0
+            spanId: 1
+            spanLayer: Http
+            startTime: gt 0
+            endTime: gt 0
+            componentId: 8002
+            isError: false
+            spanType: Exit
+            peer: 127.0.0.1:9501
+            skipAnalysis: false
+            tags:
+              - { key: url, value: "http://127.0.0.1:9501/" }
+              - { key: status_code, value: "200" }
+          - operationName: /
+            parentSpanId: 0
+            spanId: 2
+            spanLayer: Http
+            startTime: gt 0
+            endTime: gt 0
+            componentId: 8002
+            isError: false
+            spanType: Exit
+            peer: 127.0.0.1:9502
+            skipAnalysis: false
+            tags:
+              - { key: url, value: "http://127.0.0.1:9502/" }
+              - { key: status_code, value: "200" }
+          - operationName: GET:/curl
+            parentSpanId: -1
+            spanId: 0
+            spanLayer: Http
+            startTime: gt 0
+            endTime: gt 0
+            componentId: 8001
+            isError: false
+            spanType: Entry
+            peer: ""
+            skipAnalysis: false
+            tags:
+              - { key: url, value: /curl }
+              - { key: http.method, value: GET }
+              - { key: http.status_code, value: "200" }
+  - serviceName: skywalking-agent-test-2-swoole
+    segmentSize: 1
+    segments:
+      - segmentId: "not null"
+        spans:
+          - operationName: GET:/
+            parentSpanId: -1
+            spanId: 0
+            spanLayer: Http
+            startTime: gt 0
+            endTime: gt 0
+            componentId: 8001
+            isError: false
+            spanType: Entry
+            peer: ""
+            skipAnalysis: false
+            tags:
+              - { key: url, value: / }
+              - { key: http.method, value: GET }
+              - { key: http.status_code, value: "200" }
+            refs:
+              - {
+                  parentEndpoint: /,
+                  networkAddress: "127.0.0.1:9502",
+                  refType: CrossProcess,
+                  parentSpanId: 2,
+                  parentTraceSegmentId: "not null",
+                  parentServiceInstance: "not null",
+                  parentService: skywalking-agent-test-1-swoole,
+                  traceId: "not null",
+                }
diff --git a/tests/e2e.rs b/tests/e2e.rs
index 12f9e80..47a6093 100644
--- a/tests/e2e.rs
+++ b/tests/e2e.rs
@@ -15,7 +15,9 @@
 
 mod common;
 
-use crate::common::{COLLECTOR_HTTP_ADDRESS, HTTP_CLIENT, PROXY_SERVER_1_ADDRESS};
+use crate::common::{
+    COLLECTOR_HTTP_ADDRESS, HTTP_CLIENT, PROXY_SERVER_1_ADDRESS, SWOOLE_SERVER_1_ADDRESS,
+};
 use reqwest::{header::CONTENT_TYPE, RequestBuilder, StatusCode};
 use std::{
     panic::{catch_unwind, resume_unwind},
@@ -46,6 +48,7 @@
 async fn run_e2e() {
     request_fpm_curl().await;
     request_fpm_pdo().await;
+    request_swoole_curl().await;
     sleep(Duration::from_secs(3)).await;
     request_collector_validate().await;
 }
@@ -66,6 +69,14 @@
     .await;
 }
 
+async fn request_swoole_curl() {
+    request_common(
+        HTTP_CLIENT.get(format!("http://{}/curl", SWOOLE_SERVER_1_ADDRESS)),
+        "ok",
+    )
+    .await;
+}
+
 async fn request_collector_validate() {
     request_common(
         HTTP_CLIENT
diff --git a/tests/php/swoole/main.1.php b/tests/php/swoole/main.1.php
new file mode 100644
index 0000000..8d0bc7a
--- /dev/null
+++ b/tests/php/swoole/main.1.php
@@ -0,0 +1,82 @@
+<?php
+
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements.  See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance with
+// the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+use Webmozart\Assert\Assert;
+
+require_once dirname(__DIR__) . "/vendor/autoload.php";
+
+extension_loaded('swoole') or die("extension swoole not loaded");
+
+$http = new Swoole\Http\Server('127.0.0.1', 9501);
+
+$http->set([
+    'reactor_num' => 3,
+    'worker_num' => 3,
+    'enable_coroutine' => true,
+    'hook_flags' => 0,
+]);
+
+$http->on('start', function ($server) {
+    echo "Swoole http server is started at http://127.0.0.1:9501\n";
+});
+
+$http->on('request', function ($request, $response) {
+    try {
+        switch ($request->server['request_uri']) {
+        case "/":
+            break;
+
+        case "/curl":
+            {
+                $ch = curl_init();
+                curl_setopt($ch, CURLOPT_URL, "http://127.0.0.1:9501/");
+                curl_setopt($ch, CURLOPT_TIMEOUT, 10);
+                curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
+                curl_setopt($ch, CURLOPT_HEADER, 0);
+                $output = curl_exec($ch);
+                curl_close($ch);
+                Assert::same($output, "ok");
+            }
+
+            {
+                $ch = curl_init();
+                curl_setopt($ch, CURLOPT_URL, "http://127.0.0.1:9502/");
+                curl_setopt($ch, CURLOPT_TIMEOUT, 10);
+                curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
+                curl_setopt($ch, CURLOPT_HEADER, 0);
+                $output = curl_exec($ch);
+                curl_close($ch);
+                Assert::same($output, "ok");
+            }
+
+            break;
+
+        default:
+            throw new DomainException("Unknown operation");
+        }
+
+        $response->header('Content-Type', 'text/plain');
+        $response->end('ok');
+
+    } catch (Exception $e) {
+        $response->status(500);
+        $response->header('Content-Type', 'text/plain');
+        $response->end($e->getMessage() . "\n" . $e->getTraceAsString());
+    }
+});
+
+$http->start();
diff --git a/tests/php/swoole/main.2.php b/tests/php/swoole/main.2.php
new file mode 100644
index 0000000..1e1a24e
--- /dev/null
+++ b/tests/php/swoole/main.2.php
@@ -0,0 +1,57 @@
+<?php
+
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements.  See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance with
+// the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+use Webmozart\Assert\Assert;
+
+require_once dirname(__DIR__) . "/vendor/autoload.php";
+
+extension_loaded('swoole') or die("extension swoole not loaded");
+
+$http = new Swoole\Http\Server('127.0.0.1', 9502);
+
+$http->set([
+    'reactor_num' => 3,
+    'worker_num' => 3,
+    'enable_coroutine' => false,
+    'hook_flags' => 0,
+]);
+
+$http->on('start', function ($server) {
+    echo "Swoole http server is started at http://127.0.0.1:9502\n";
+});
+
+$http->on('request', function ($request, $response) {
+    try {
+        switch ($request->server['request_uri']) {
+        case "/":
+            break;
+
+        default:
+            throw new DomainException("Unknown operation");
+        }
+
+        $response->header('Content-Type', 'text/plain');
+        $response->end('ok');
+
+    } catch (Exception $e) {
+        $response->status(500);
+        $response->header('Content-Type', 'text/plain');
+        $response->end($e->getMessage() . "\n" . $e->getTraceAsString());
+    }
+});
+
+$http->start();