Adapting NFC Functionality for QWRT on Xiaomi BE10000 Router
English Translation Title: Adapting NFC Functionality for QWRT on Xiaomi BE10000 Router Analysis After flashing the Xiaomi BE10000 with QWRT, the device's network potential is indeed greatly unleashed. Advanced features such as 2.5G optical modules and SFP+ interfaces work perfectly. The only drawback is that the factory NFC "tap to connect to Wi-Fi" feature no longer works. After researching, the NFC tag is essentially an EEPROM chip mounted on the motherboard. Using i2cdetect for scanning: root@QWRT:~# i2cdetect -l i2c-1 i2c QUP I2C adapter I2C adapter i2c-2 i2c QUP I2C adapter I2C adapter i2c-0 i2c QUP I2C adapter I2C adapter root@QWRT:~# i2cdetect -y -r 0 0 1 2 3 4 5 6 7 8 9 a b c d e f 00: -- -- -- -- -- -- -- -- -- -- -- -- -- 10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 50: -- -- -- -- 54 -- -- -- -- -- -- -- -- -- -- -- 60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 70: -- -- -- -- -- -- -- -- The scan results quickly pinpoint the device attached to I2C bus 0 at physical address 0x54. NFC tap-to-connect-to-Wi-Fi uses standard NDEF format data (Ref: Wi-Fi Simple Configuration — ndeflib 0.3.2 documentation). Therefore, simply writing the data into the EEPROM according to the standard can restore the NFC tap-to-connect functionality. Implementation This article discusses only low-level hardware driver adaptation and NDEF standard protocol encapsulation for the OpenWrt/QWRT system. The hardware parameters mentioned are derived from public specification documents and generic I2C debugging tools. This project is for personal research interest and does not include or distribute any vendor-proprietary binary code. It is intended for technical exchange and learning only. Do not use for commercial purposes. Any risk of device damage resulting from attempts described herein shall be borne solely by the reader. Extract Data and Construct NDEF Payload To automatically update NFC data based on Wi-Fi credentials, we first need to obtain the current Wi-Fi SSID and password. On OpenWrt, these configurations are managed entirely by UCI (Unified Configuration Interface). Therefore, we only need to read the wireless configuration file from UCI. To ensure compatibility with modern smartphones, early devices typically used a Device Password Token to trigger WPS negotiation, but modern Android/iOS systems have restricted this behavior. For broader compatibility, we must follow the Wi-Fi Simple Configuration (WSC) specification and package the configuration as a WLAN Configuration Token (credential configuration token). (Ref: Wi-Fi Simple Configuration — ndeflib 0.3.2 documentation) Map OpenWrt's wireless encryption modes (e.g., WPA2, WPA3-SAE) precisely to the hex codes defined by the WSC specification: 0x1003: Authentication Type 0x100F: Encryption Type 0x1045: SSID 0x1027: Network Key (password) Through a script, we automatically traverse and select the primary AP bridged to lan (e.g., wifi0), convert its attributes to hex strings, and produce a standard NDEF payload. Write to NFC EEPROM When the NFC EEPROM receives a long string of NDEF data, writing too quickly or in excessively large blocks per write can easily cause the chip's I2C state machine to lock up. After testing, we chose to use the i2ctransfer tool for atomic fragmented writes. Two critical timing details: Due to communication limitations, each loop slices only 4 bytes, with auto-incrementing register addresses. Between each 4-byte block write, a 10 ms delay is enforced to allow sufficient internal erase/write time for the chip. Finally, any remaining data less than 4 bytes is padded with 0x00. Automatically Trigger Writes on Wi-Fi Configuration Changes To closely follow OpenWrt's architecture, we initially tried using hooks but found they often failed. Eventually, three fallback layers were added: LuCI frontend trigger: Register a hook under /etc/uci-defaults/ to bind the NFC sync script to the system's ucitrack mechanism. When a user modifies the Wi-Fi password in LuCI and clicks "Save & Apply", the system automatically updates the NFC data in the background. Hotplug layer: Add a hotplug event listener in /etc/hotplug.d/iface/70-nfc. When the router's lan or wifi interface changes to ifup state, the system automatically triggers the sync. Cron job: If none of the above triggers work, a cron job forces a check every 15 seconds to determine if an NFC update is needed. Additionally, considering that the NFC EEPROM has limited write endurance, if the network interface restarts even once and triggers a full rewrite, the chip would soon wear out. Therefore, a simple hash check mechanism is introduced in the underlying nfc-sync script: When the script is awakened, it first extracts the current wireless configuration and calculates its MD5 hash. It compares this hash with the old hash cached in /var/run/nfc-wireless.md5. Only when the MD5 value actually changes does it issue the I2C write command. Otherwise, the process terminates immediately. Combined with a concurrent file lock (/var/lock/nfc-sync.lock), this logic ensures that the NFC hardware's lifespan is absolutely protected against any network flapping or multiple concurrent events. After testing, automatic updating works as expected: !Pasted image 20260525230448.png Code repository: KaguraiYoRoy/be10000-qwrt-nfc: NFC Userland Implementation of QWRT for Xiaomi BE10000 (RC01) Router References: Wi-Fi Simple Configuration — ndeflib 0.3.2 documentation
Configuring a Simple Multi-language Solution for Typecho
I wanted to add internationalization support to my blog, providing a separate English version for each post and page. However, after searching online, I found that Typecho has poor support for i18n. Eventually, I designed my own solution and am documenting it here. This article assumes you have some basic understanding of PHP, Nginx, and Typecho's core logic. Analysis Requirements Need to provide both Chinese and English versions for each post and page. Need to configure a language switcher so users can easily switch languages on the frontend. Need search engines to correctly identify and index the multi-language versions of the content. Proposed Solution There are roughly two main schemes for distinguishing between Chinese and English content: Use a separate parameter, like accessing posts with /?lang=zh-CN and /?lang=en-US. However, this scheme is relatively difficult to implement and less friendly for search engine indexing. Distinguish via the URL path, e.g., https://<host>/article for the Chinese page and https://<host>/en/article for the English page. This is simpler to configure (essentially setting up two separate Typecho instances) and is more search engine friendly. The challenge is that comments and view counts need to be manually synchronized. After summarizing, I chose the second scheme and planned to implement multi-language support by creating a new Typecho instance directly under the /en/ path. Implementation Plan First, duplicate the blog instance into two copies: one for Chinese and one for English, then translate the English copy. Modify the frontend code to implement the language switcher. To ensure the article URLs between the two sites only differ by the /en prefix, the cid (content ID) for corresponding articles must be the same. Since cid is auto-incremented based on the order of creating posts and attachments, I plan to write a sync plugin. When a post is published on the Chinese site, it automatically inserts a corresponding article with the same cid in the English database. Modify the SiteMap plugin. Because a sitemap cannot both contain page links and references to other sitemaps, the main site needs to create two sitemaps: one main sitemap containing the Chinese site pages, and another index sitemap responsible for indexing both the Chinese and English sitemaps. Add the hreflang attribute within the section to inform search engines about the multi-language handling. Link the view counts and like counts from the English site to the Chinese database. Sync the comments between two instances. Let's Do It Create the English Instance Copy the entire website directory and place it in the /en/ folder under the original web root. Also, duplicate the database; I named the new one typecho_en. Next, configure URL rewrite (pseudo-static) rules for both instances: location /en/ { if (!-e $request_filename) { rewrite ^(.*)$ /en/index.php$1 last; } } location / { if (!-e $request_filename) { rewrite ^(.*)$ /index.php$1 last; } } The reason for wrapping the main Chinese instance's rules in a location block is that during testing, I found that without it, the English instance might be parsed as part of the Chinese instance, leading to 404 errors. Also, modify the database configuration in <webroot>/en/config.inc.php to point to the English instance's database. At this point, accessing <host>/en/ should display a site identical to the main Chinese site. Modify Typecho Language This step might be optional since the frontend language is largely determined by the theme. Changing Typecho's backend language isn't strictly necessary but helps for consistency (and makes it easy to tell which admin panel you're in!). Simply refer to the official Typecho multi-language support on GitHub. Download the language pack from the Releases and extract it to <webroot>/en/usr/langs/. Then, navigate to https://<host>/en/admin/options-general.php, where you should see the language setting option. Change it to English. Translate the Theme This is the most tedious step. I use the Joe theme. Go to <webroot>/en/usr/themes/Joe and translate all the Chinese text related to display into English. There's no very convenient method here; machine translation often sounds awkward, so I opted for manual translation. Note that some frontend configurations are within JS files, not just PHP source files. These need translation too. Translate Articles This step is self-explanatory. Translate the articles under /en/ one by one into English and save them. Configure Article Sync Publishing This step ensures the cid remains synchronized between corresponding articles on both sites. Since cid relates to the access URL, keeping them in sync simplifies the language switcher configuration later—just adding or removing /en from the host. cid is an auto-incrementing primary key field in the typecho_contents table. Its assignment is also related to attachments in Typecho. Since I plan to upload all attachments to the Chinese site, without special handling, the cid values can easily become misaligned, increasing subsequent work. Therefore, my chosen solution is to use AI to help write a plugin that triggers when the Chinese site publishes an article. It reads the cid assigned by the Chinese site and writes a corresponding entry into the English site's database. Create the file <webroot>/usr/plugins/SyncToEnglish/Plugin.php and fill it with the following content: <?php if (!defined('__TYPECHO_ROOT_DIR__')) exit; /** * Sync Chinese Articles to English Database * * @package SyncToEnglish * @author ChatGPT, iYoRoy * @version 1.0.0 * @link https://example.com */ class SyncToEnglish_Plugin implements Typecho_Plugin_Interface { public static function activate() { Typecho_Plugin::factory('Widget_Contents_Post_Edit')->finishPublish = [__CLASS__, 'push']; return 'SyncToEnglish plugin activated: Empty corresponding articles will be automatically created in the English database when Chinese articles are published.'; error_log("[SyncToEnglish] Plugin activated successfully"); } public static function deactivate() { return 'SyncToEnglish plugin deactivated'; } public static function config(Typecho_Widget_Helper_Form $form) { $host = new Typecho_Widget_Helper_Form_Element_Text('host', NULL, 'localhost', _t('English DB Host')); $user = new Typecho_Widget_Helper_Form_Element_Text('user', NULL, 'root', _t('English DB Username')); $password = new Typecho_Widget_Helper_Form_Element_Password('password', NULL, NULL, _t('English DB Password')); $database = new Typecho_Widget_Helper_Form_Element_Text('database', NULL, 'typecho_en', _t('English DB Name')); $port = new Typecho_Widget_Helper_Form_Element_Text('port', NULL, '3306', _t('English DB Port')); $charset = new Typecho_Widget_Helper_Form_Element_Text('charset', NULL, 'utf8mb4', _t('Charset')); $prefix = new Typecho_Widget_Helper_Form_Element_Text('prefix', NULL, 'typecho_', _t('Table Prefix')); $form->addInput($host); $form->addInput($user); $form->addInput($password); $form->addInput($database); $form->addInput($port); $form->addInput($charset); $form->addInput($prefix); } public static function personalConfig(Typecho_Widget_Helper_Form $form) {} public static function push($contents, $widget) { $options = Helper::options(); $config = $options->plugin('SyncToEnglish'); // Get article info from Chinese database $cnDb = Typecho_Db::get(); if (is_array($contents) && isset($contents['cid'])) { $cid = $contents['cid']; $title = $contents['title']; } elseif (is_object($contents) && isset($contents->cid)) { $cid = $contents->cid; $title = $contents->title; } else { $db = Typecho_Db::get(); $row = $db->fetchRow($db->select()->from('table.contents')->order('cid', Typecho_Db::SORT_DESC)->limit(1)); $cid = $row['cid']; $title = $row['title']; error_log("[SyncToEnglish DEBUG] CID not found in param, fallback to latest cid={$cid}\n", 3, __DIR__ . '/debug.log'); } $article = $cnDb->fetchRow($cnDb->select()->from('table.contents')->where('cid = ?', $cid)); if (!$article) return; $enDb = new Typecho_Db('Mysql', $config->prefix); $enDb->addServer([ 'host' => $config->host, 'user' => $config->user, 'password' => $config->password, 'charset' => $config->charset, 'port' => (int)$config->port, 'database' => $config->database ], Typecho_Db::READ | Typecho_Db::WRITE); try { $exists = $enDb->fetchRow($enDb->select()->from('table.contents')->where('cid = ?', $article['cid'])); if ($exists) { $enDb->query($enDb->update('table.contents') ->rows([ // 'title' => $article['title'], 'slug' => $article['slug'], 'modified' => $article['modified'] ]) ->where('cid = ?', $article['cid']) ); } else { $enDb->query($enDb->insert('table.contents')->rows([ 'cid' => $article['cid'], 'title' => $article['title'], 'slug' => $article['slug'], 'created' => $article['created'], 'modified' => $article['modified'], 'type' => $article['type'], 'status' => $article['status'], 'authorId' => $article['authorId'], 'views' => 0, 'text' => $article['text'], 'allowComment' => $article['allowComment'], 'allowFeed' => $article['allowFeed'], 'allowPing' => $article['allowPing'] ])); } } catch (Exception $e) { error_log('[SyncToEnglish] Sync failed: ' . $e->getMessage()); } } } Then, go to the admin backend, enable the plugin, and configure the English database information. After completion, publishing an article on the Chinese site should automatically publish an article with the same cid on the English site. Configure the Language Switcher Since we have synchronized the article cid, switching languages now only requires modifying the URL by adding or removing the /en/ prefix. We can create a switcher using PHP and place it in the theme's header: Language <path d="M561.873 725.165c-11.262 11.262-26.545 21.72-41.025 18.502-14.479 2.413-28.154-8.849-39.415-18.502L133.129 375.252c-17.697-17.696-17.697-46.655 0-64.352s46.655-17.696 64.351 0l324.173 333.021 324.977-333.02c17.696-17.697 46.655-17.697 64.351 0s17.697 46.655 0 64.351L561.873 725.165z" fill="var(--main)" /> <?php // Get the current full URL $current_url = $_SERVER['REQUEST_URI']; $host = $_SERVER['HTTP_HOST']; // Check if there is an English prefix "/en/" if (strpos($current_url, '/en/') === 0) { $current_url = substr_replace($current_url, '', 0, 3); } $new_url_cn = 'https://' . $host . $current_url; $new_url_en = 'https://' . $host . '/en' . $current_url; // Generate the two hyperlinks echo '简体中文'; echo 'English'; ?> This needs to be added to both the Chinese and English instances. After this, the language selector should be available globally. For the Joe theme I use, separate language selectors needed to be written for mobile and PC views. Modify the SiteMap Plugin To help search engines index the English pages faster, I decided to modify the SiteMap plugin to include the English site's pages. There are two types of sitemaps: sitemapindex (for indexing sub-sitemaps) and urlset (for containing page URLs). I use the joyqi/typecho-plugin-sitemap plugin. Based on this, I changed the default /sitemap.xml to a sitemapindex, created a new route /sitemap_cn.xml to hold the Chinese site's sitemap, left the English site's plugin unchanged (its sitemap remains at /en/sitemap.xml), and had the main index sitemap reference both /sitemap_cn.xml and /en/sitemap.xml. Modify the SiteMap's Plugin.php: /** * Activate plugin method, if activated failed, throw exception will disable this plugin. */ public static function activate() { Helper::addRoute( - 'sitemap', + 'sitemap_index', '/sitemap.xml', Generator::class, - 'generate', + 'generate_index', 'index' ); + Helper::addRoute( + 'sitemap_cn', + '/sitemap_cn.xml', + Generator::class, + 'generate_cn', + 'index' + ); } /** * Deactivate plugin method, if deactivated failed, throw exception will enable this plugin. */ public static function deactivate() { - Helper::removeRoute('sitemap'); + Helper::removeRoute('sitemap_index'); + Helper::removeRoute('sitemap_cn'); } {collapse} {collapse-item label="Complete code"} <?php namespace TypechoPlugin\Sitemap; use Typecho\Plugin\PluginInterface; use Typecho\Widget\Helper\Form; use Utils\Helper; if (!defined('__TYPECHO_ROOT_DIR__')) { exit; } /** * Plugin to automatically generate a sitemap for Typecho. * The sitemap URL is: http(s)://yourdomain.com/sitemap.xml * * @package Sitemap Plugin * @author joyqi * @version 1.0.0 * @since 1.2.1 * @link https://github.com/joyqi/typecho-plugin-sitemap */ class Plugin implements PluginInterface { /** * Activate plugin method, if activated failed, throw exception will disable this plugin. */ public static function activate() { Helper::addRoute( 'sitemap_index', '/sitemap.xml', Generator::class, 'generate_index', 'index' ); Helper::addRoute( 'sitemap_cn', '/sitemap_cn.xml', Generator::class, 'generate_cn', 'index' ); } /** * Deactivate plugin method, if deactivated failed, throw exception will enable this plugin. */ public static function deactivate() { Helper::removeRoute('sitemap_index'); Helper::removeRoute('sitemap_cn'); } /** * Plugin config panel render method. * * @param Form $form */ public static function config(Form $form) { $sitemapBlock = new Form\Element\Checkbox( 'sitemapBlock', [ 'posts' => _t('Generate post links'), 'pages' => _t('Generate page links'), 'categories' => _t('Generate category links'), 'tags' => _t('Generate tag links'), ], ['posts', 'pages', 'categories', 'tags'], _t('Sitemap Display') ); $updateFreq = new Form\Element\Select( 'updateFreq', [ 'daily' => _t('Daily'), 'weekly' => _t('Weekly'), 'monthly' => _t('Monthly or less often'), ], 'daily', _t('Update Frequency') ); // $externalSitemap = new Typecho_Widget_Helper_Form_Element_Text('externalSitemap', NULL, '', _t('Additional Sitemap')); $form->addInput($sitemapBlock->multiMode()); $form->addInput($updateFreq); // $form->addInput($externalSitemap); } /** * Plugin personal config panel render method. * * @param Form $form */ public static function personalConfig(Form $form) { // TODO: Implement personalConfig() method. } } {/collapse-item} {/collapse} Modify the SiteMap's Generator.php: class Generator extends Contents { + public function generate_index(){ + $sitemap = '<?xml version="1.0" encoding="UTF-8"?> +<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> + <sitemap> + <loc>https://www.iyoroy.cn/sitemap_cn.xml</loc> + </sitemap> + <sitemap> + <loc>https://www.iyoroy.cn/en/sitemap.xml</loc> + </sitemap> +</sitemapindex>'; + $this->response->throwContent($sitemap, 'text/xml'); + } + /** * @return void */ - public function generate() + public function generate_cn() { $sitemap = '<?xml version="1.0" encoding="' . $this->options->charset . '"?>' . PHP_EOL; ... {collapse} {collapse-item label="Complete code"} <?php namespace TypechoPlugin\Sitemap; use Widget\Base\Contents; use Widget\Contents\Page\Rows; use Widget\Contents\Post\Recent; use Widget\Metas\Category\Rows as CategoryRows; use Widget\Metas\Tag\Cloud; if (!defined('__TYPECHO_ROOT_DIR__')) { exit; } /** * Sitemap Generator */ class Generator extends Contents { public function generate_index(){ $sitemap = '<?xml version="1.0" encoding="UTF-8"?> <sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> <sitemap> <loc>https://www.iyoroy.cn/sitemap_cn.xml</loc> </sitemap> <sitemap> <loc>https://www.iyoroy.cn/en/sitemap.xml</loc> </sitemap> </sitemapindex>'; $this->response->throwContent($sitemap, 'text/xml'); } /** * @return void */ public function generate_cn() { $sitemap = '<?xml version="1.0" encoding="' . $this->options->charset . '"?>' . PHP_EOL; $sitemap .= '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"' . ' xmlns:news="http://www.google.com/schemas/sitemap-news/0.9"' . ' xmlns:xhtml="http://www.w3.org/1999/xhtml"' . ' xmlns:image="http://www.google.com/schemas/sitemap-image/1.1"' . ' xmlns:video="http://www.google.com/schemas/sitemap-video/1.1">' . PHP_EOL; // add homepage $sitemap .= <<<EOF <url> <loc>{$this->options->siteUrl}</loc> <changefreq>daily</changefreq> <priority>1.0</priority> </url> EOF; // add posts if (in_array('posts', $this->options->plugin('Sitemap')->sitemapBlock)) { $postsCount = $this->size($this->select() ->where('table.contents.status = ?', 'publish') ->where('table.contents.created < ?', $this->options->time) ->where('table.contents.type = ?', 'post')); $posts = Recent::alloc(['pageSize' => $postsCount]); $freq = $this->options->plugin('Sitemap')->updateFreq ==='monthly' ? 'monthly' : 'weekly'; while ($posts->next()) { $sitemap .= <<<EOF <url> <loc>{$posts->permalink}</loc> <changefreq>{$freq}</changefreq> <lastmod>{$posts->date->format('c')}</lastmod> <priority>0.8</priority> </url> EOF; } } // add pages if (in_array('pages', $this->options->plugin('Sitemap')->sitemapBlock)) { $pages = Rows::alloc(); $freq = $this->options->plugin('Sitemap')->updateFreq ==='monthly' ? 'yearly' : 'monthly'; while ($pages->next()) { $sitemap .= <<<EOF <url> <loc>{$pages->permalink}</loc> <changefreq>{$freq}</changefreq> <lastmod>{$pages->date->format('c')}</lastmod> <priority>0.5</priority> </url> EOF; } } // add categories if (in_array('categories', $this->options->plugin('Sitemap')->sitemapBlock)) { $categories = CategoryRows::alloc(); $freq = $this->options->plugin('Sitemap')->updateFreq; while ($categories->next()) { $sitemap .= <<<EOF <url> <loc>{$categories->permalink}</loc> <changefreq>{$freq}</changefreq> <priority>0.6</priority> </url> EOF; } } // add tags if (in_array('tags', $this->options->plugin('Sitemap')->sitemapBlock)) { $tags = Cloud::alloc(); $freq = $this->options->plugin('Sitemap')->updateFreq; while ($tags->next()) { $sitemap .= <<<EOF <url> <loc>{$tags->permalink}</loc> <changefreq>{$freq}</changefreq> <priority>0.4</priority> </url> EOF; } } $sitemap .= '</urlset>'; $this->response->throwContent($sitemap, 'text/xml'); } } {/collapse-item} {/collapse} Please replace the blog URL in the code with your own. (I was too busy recently to create a separate configuration page, so I hardcoded the Sitemap URLs into the plugin for now.) Disable and then re-enable the plugin. Visiting https://<host>/sitemap.xml should now show the sitemap index: <sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> <sitemap> <loc>https://www.iyoroy.cn/sitemap_cn.xml</loc> </sitemap> <sitemap> <loc>https://www.iyoroy.cn/en/sitemap.xml</loc> </sitemap> </sitemapindex> You should also be able to see that search engines like Bing Webmaster Tools have detected the English site's sitemap: Add hreflang This step informs search engines that the current page has multi-language versions, allowing them to show the appropriate page based on user language preference or location. We need to insert link tags like the following format within the section: Here, hreflang="x-default" indicates the default language for the page. The value of hreflang is composed of an ISO 639-1 language code and an optional ISO 3166-1 Alpha-2 region code (e.g., distinguishing between en, en-US and en-UK). Add the following content to the relevant section of your theme's : <?php // Get the current full URL $current_url = $_SERVER['REQUEST_URI']; $host = $_SERVER['HTTP_HOST']; // Check if there is an English prefix "/en/" if (strpos($current_url, '/en/') === 0) { $current_url = substr_replace($current_url, '', 0, 3); } $new_url_cn = 'https://' . $host . $current_url; $new_url_en = 'https://' . $host . '/en' . $current_url; // Generate the link tags echo ' '; echo ' '; echo ' '; ?> This needs to be added to both the Chinese and English sites. After this, you should find the corresponding hreflang configuration in the section of your website pages. Sync Like Counts and View Counts This step is highly theme-dependent and might not apply to all themes. I use the Joe theme, which handles reading and writing like counts and view counts to the database directly. I modified the English instance's theme code to read and write these values directly from/to the Chinese instance's database. Modify the function that retrieves view counts in <webroot>/en/usr/themes/Joe/core/function.php: /* Query Post Views */ function _getViews($item, $type = true) { - $db = Typecho_Db::get(); + // $db = Typecho_Db::get(); + $db = new Typecho_Db('Mysql', 'typecho_' /* Prefix */); + $db->addServer([ + 'host' => 'mysql', + 'user' => 'typecho', + 'password' => '[CENSORED]', + 'charset' => 'utf8mb4', + 'port' => 3306, + 'database' => 'typecho' + ], Typecho_Db::READ | Typecho_Db::WRITE); $result = $db->fetchRow($db->select('views')->from('table.contents')->where('cid = ?', $item->cid))['views']; if ($type) echo number_format($result); else return number_format($result); } Modify the function that retrieves like counts in <webroot>/en/usr/themes/Joe/core/function.php: /* Query Post Like Count */ function _getAgree($item, $type = true) { - $db = Typecho_Db::get(); + // $db = Typecho_Db::get(); + $db = new Typecho_Db('Mysql', 'typecho_' /* Prefix */); + $db->addServer([ + 'host' => 'mysql', + 'user' => 'typecho', + 'password' => '[CENSORED]', + 'charset' => 'utf8mb4', + 'port' => 3306, + 'database' => 'typecho' + ], Typecho_Db::READ | Typecho_Db::WRITE); $result = $db->fetchRow($db->select('agree')->from('table.contents')->where('cid = ?', $item->cid))['agree']; if ($type) echo number_format($result); else return number_format($result); } Modify the code displaying view counts on the homepage in <webroot>/en/usr/themes/Joe/core/route.php: $result[] = array( "mode" => $item->fields->mode ? $item->fields->mode : 'default', "image" => _getThumbnails($item), "time" => date('Y-m-d', $item->created), "created" => date('d/m/Y', $item->created), "title" => $item->title, "abstract" => _getAbstract($item, false), "category" => $item->categories, - "views" => number_format($item->views), + // "views" => number_format($item->views), + "views" => _getViews($item, false), "commentsNum" => number_format($item->commentsNum), - "agree" => number_format($item->agree), + // "agree" => number_format($item->agree), + "agree" => _getAgree($item, false), "permalink" => $item->permalink, "lazyload" => _getLazyload(false), "type" => "normal" ); The code displaying view counts on the article page itself already uses _getViews, so it doesn't need modification. Modify the code that increments view counts: /* Increase View Count - Tested √ */ function _handleViews($self) { $self->response->setStatus(200); $cid = $self->request->cid; /* SQL injection check */ if (!preg_match('/^\d+$/', $cid)) { return $self->response->throwJson(array("code" => 0, "data" => "Illegal request! Blocked!")); } - $db = Typecho_Db::get(); + // $db = Typecho_Db::get(); + $db = new Typecho_Db('Mysql', 'typecho_' /* Prefix */); + $db->addServer([ + 'host' => 'mysql', + 'user' => 'typecho', + 'password' => '[CENSORED]', + 'charset' => 'utf8mb4', + 'port' => 3306, + 'database' => 'typecho' + ], Typecho_Db::READ | Typecho_Db::WRITE); $row = $db->fetchRow($db->select('views')->from('table.contents')->where('cid = ?', $cid)); if (sizeof($row) > 0) { Modify the code for liking and unliking: /* Like and Unlike - Tested √ */ function _handleAgree($self) { $self->response->setStatus(200); $cid = $self->request->cid; $type = $self->request->type; /* SQL injection check */ if (!preg_match('/^\d+$/', $cid)) { return $self->response->throwJson(array("code" => 0, "data" => "Illegal request! Blocked!")); } /* SQL injection check */ if (!preg_match('/^[agree|disagree]+$/', $type)) { return $self->response->throwJson(array("code" => 0, "data" => "Illegal request! Blocked!")); } - $db = Typecho_Db::get(); + // $db = Typecho_Db::get(); + $db = new Typecho_Db('Mysql', 'typecho_' /* Prefix */); + $db->addServer([ + 'host' => 'mysql', + 'user' => 'typecho', + 'password' => '[CENSORED]', + 'charset' => 'utf8mb4', + 'port' => 3306, + 'database' => 'typecho' + ], Typecho_Db::READ | Typecho_Db::WRITE); $row = $db->fetchRow($db->select('agree')->from('table.contents')->where('cid = ?', $cid)); if (sizeof($row) > 0) { After making these changes and saving, visiting the English site should show view counts and like counts synchronized with the Chinese site. Sync Comments I initially thought about creating a plugin that hooks into the comment submission function to simultaneously insert comment data into the other instance's database. However, I found that my Joe theme already hooks into this, and adding another hook might cause conflicts. Therefore, I directly edited the Joe theme's code. Edit <webroot>/index/usr/themes/Joe/core/factory.php: <?php require_once("phpmailer.php"); require_once("smtp.php"); /* Enhanced Comment Interception */ Typecho_Plugin::factory('Widget_Feedback')->comment = array('Intercept', 'message'); class Intercept { public static function message($comment) { ... Typecho_Cookie::delete('__typecho_remember_text'); + + $db = new Typecho_Db('Mysql', 'typecho_' /* Prefix */); + $db->addServer([ + 'host' => 'mysql', + 'user' => 'typecho_en', + 'password' => '[CENSORED]', + 'charset' => 'utf8mb4', + 'port' => 3306, + 'database' => 'typecho_en' + ], Typecho_Db::READ | Typecho_Db::WRITE); + + $row = [ + 'coid' => $comment['coid'], // Must include the newly generated comment ID + 'cid' => $comment['cid'], + 'created' => $comment['created'], + 'author' => $comment['author'], + 'authorId' => $comment['authorId'], + 'ownerId' => $comment['ownerId'], + 'mail' => $comment['mail'], + 'url' => $comment['url'], + 'ip' => $comment['ip'], + 'agent' => $comment['agent'], + 'text' => $comment['text'], + 'type' => $comment['type'], + 'status' => $comment['status'], + 'parent' => $comment['parent'] + ]; + + // Insert data into the target database's `comments` table + $db->query($db->insert('typecho_comments')->rows($row)); return $comment; } } ... Perform the same operation on the English instance, inserting comments into the Chinese database. One issue with this scheme is that if you need to delete spam comments, you must delete them separately in both instances. I'll fix that later (maybe). Reference: typecho/languages - GitHub
Cross-Platform Service Programming Diary Ep.2 - Inter-Process Communication (IPC)
Previously The previous article implemented unified log management. This article implements inter-process message communication, i.e., IPC. Analysis Windows On Windows, inter-process communication is primarily achieved through Pipes. Pipes are further divided into Named Pipes and Anonymous Pipes. Anonymous pipes are unidirectional and are typically used for communication between a parent and child process[2]. Named pipes, however, can be unidirectional or duplex and support one-to-many communication[3]. As the name implies, the only way to identify a named pipe is by its name, so two processes can communicate as long as they connect to the named pipe with the same name. We need to achieve bidirectional communication between processes, so we use named pipes. The general idea is: a process starts in server mode (receiver), creates a thread, creates a named pipe, and listens for messages within the pipe. When the pipe is connected, it reads data from it; when a process starts as a sender, it attempts to connect to a pipe with the same name and writes the message content. Linux On Linux, sockets are typically used for inter-process communication. However, unlike listening on a port, IPC usually involves listening on a sock file[5]. Common service applications like the Docker daemon and MySQL use this method. Thus, the general idea is as follows: a process started in server mode creates a socket listener and waits to receive messages from it; the sender connects to the socket and sends a message. Similar to the name of the named pipe mentioned above, the socket maps to a unique .sock file. The sender just needs to open this file to send the message. (In practice, it's not opened in the conventional file manner but using socket-specific methods[5].) Code Implementation Initialization To use a common main codebase, I used the same approach as the previous article, differentiating system types via macro definitions, placing the Windows and Linux code in the header files service-windows.h and service-linux.h respectively: #ifdef _WIN32 #include "service-windows.h" #elif defined(__linux__) #include "service-linux.h" #endif When the receiver process starts, it creates a thread to handle message reception (using std::thread as the multithreading library): thread_bind = std::thread(bind_thread_main); Listener Section Windows On Windows, we simply attempt to read data from a named pipe with a specified name. Because the pipe is set to blocking mode (i.e., PIPE_WAIT is set in the DWORD dwPipeMode parameter of CreateNamedPipe below), ConnectNamedPipe will be blocking, so there's no need to worry about performance loss from constant looping. void bind_thread_main() { while (!exit_requested.load()) { HANDLE hPipe = CreateNamedPipe( PIPE_NAME, PIPE_ACCESS_DUPLEX, PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT, PIPE_UNLIMITED_INSTANCES, 1024, // Output buffer size 1024, // Input buffer size 0, // Default timeout NULL); if (hPipe == INVALID_HANDLE_VALUE) { service_log.push(LEVEL_WARN, "Failed to create pipe: %d", GetLastError()); continue; } if (ConnectNamedPipe(hPipe, NULL) || GetLastError() == ERROR_PIPE_CONNECTED) { char buffer[1024]; DWORD bytesRead; if (ReadFile(hPipe, buffer, sizeof(buffer) - 1, &bytesRead, NULL)) { buffer[bytesRead] = '\0'; m_queueMsg.push(buffer); service_log.push(LEVEL_VERBOSE, "Message received: %s", buffer); } FlushFileBuffers(hPipe); DisconnectNamedPipe(hPipe); CloseHandle(hPipe); } else { CloseHandle(hPipe); } } } Linux To prevent creation failure, the code attempts to delete any leftover sock file that wasn't cleaned up before creation, i.e., unlink(SOCKET_PATH) in the code. SOCKET_PATH is a global variable defining the path to the socket file. When creating the socket, the family is specified as AF_UNIX, indicating a UNIX socket (the .sock file type; for network sockets it would be AF_INET). The timeval code sets a timeout limit. If the accept function waits longer than the set SOCKET_TIMEOUT (in seconds), it will automatically stop blocking and return an error. After creating the socket, proceed with the normal setup for binding and listening. void bind_thread_main() { unlink(SOCKET_PATH); int server_fd = socket(AF_UNIX, SOCK_STREAM, 0); if (server_fd == -1) { service_log.push(LEVEL_FATAL, "Failed to create socket"); exit_requested.store(true); return; } struct timeval tv; tv.tv_sec = SOCKET_TIMEOUT; tv.tv_usec = 0; setsockopt(server_fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); sockaddr_un addr{}; addr.sun_family = AF_UNIX; strncpy(addr.sun_path, SOCKET_PATH, sizeof(addr.sun_path) - 1); if (bind(server_fd, (sockaddr*)&addr, sizeof(addr)) == -1) { service_log.push(LEVEL_FATAL, "Bind failed"); close(server_fd); exit_requested.store(true); return; } if (listen(server_fd, 5) == -1) { service_log.push(LEVEL_FATAL, "Listen failed"); close(server_fd); exit_requested.store(true); return; } while (!exit_requested.load()) { int client_fd = accept(server_fd, nullptr, nullptr); if (client_fd != -1) { char buffer[1024]; int bytes_read = read(client_fd, buffer, sizeof(buffer) - 1); if (bytes_read > 0) { buffer[bytes_read] = '\0'; m_queueMsg.push(buffer); service_log.push(LEVEL_VERBOSE, "Message received: %s", buffer); } close(client_fd); } else { if (errno == EWOULDBLOCK || errno == EAGAIN) { continue; } service_log.push(LEVEL_WARN, "Failed to accept socket connection"); } } } After reading a message, both code versions save the message to the blocking queue m_queueMsg. Sender Section Windows Open the specified pipe and write the message content: bool send_message(const std::string& msg) { if (!WaitNamedPipe(PIPE_NAME, NMPWAIT_WAIT_FOREVER)) { service_log.push(LEVEL_ERROR, "Failed to find valid pipe: %d", GetLastError()); return false; } HANDLE hPipe = CreateFile( PIPE_NAME, GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL); if (hPipe == INVALID_HANDLE_VALUE) { service_log.push(LEVEL_ERROR, "Failed to connect: %d", GetLastError()); return false; } DWORD bytesWritten; if (WriteFile(hPipe, msg.c_str(), (DWORD)msg.size(), &bytesWritten, NULL)) { service_log.push(LEVEL_VERBOSE, "Message sent: %s", msg.c_str()); CloseHandle(hPipe); return true; } else { service_log.push(LEVEL_ERROR, "Message (%s) send failed: %d", msg.c_str(),GetLastError()); CloseHandle(hPipe); return false; } } Linux Similarly, connect to the socket and send the data: bool send_message(const std::string& msg) { int sock = socket(AF_UNIX, SOCK_STREAM, 0); if (sock == -1) { service_log.push(LEVEL_ERROR, "Failed to create socket"); return false; } sockaddr_un addr{}; addr.sun_family = AF_UNIX; strncpy(addr.sun_path, SOCKET_PATH, sizeof(addr.sun_path) - 1); if (connect(sock, (sockaddr*)&addr, sizeof(addr)) == -1) { service_log.push(LEVEL_ERROR, "Connect failed"); close(sock); return false; } if (write(sock, msg.c_str(), msg.size()) == -1) { service_log.push(LEVEL_ERROR, "Message send failed: %s", msg.c_str()); close(sock); return false; } else { service_log.push(LEVEL_VERBOSE, "Message sent success: %s", msg.c_str()); close(sock); return true; } } Cleanup There's little to clean up on Windows, but on Linux, the socket file needs to be deleted: unlink(SOCKET_PATH); Demo Screenshots Windows Linux Sample code download: IPCTest.zip References: https://learn.microsoft.com/zh-cn/windows/win32/ipc/pipes https://learn.microsoft.com/zh-cn/windows/win32/ipc/anonymous-pipes https://learn.microsoft.com/zh-cn/windows/win32/ipc/named-pipes https://www.cnblogs.com/alantu2018/p/8493809.html https://blog.csdn.net/dog250/article/details/100998838
Cross-Platform Service Programming Diary Ep.1 - Unified Logging Management
A while ago, on a whim, I decided to write my own management program for a cross-platform console service-class application I was using, in order to add some features. Thus, I designed a simple service operation flow. {alert type="warning"} The views and solutions in this series of articles are designed by me based on my existing knowledge combined with assistance from DeepSeek. They have not been rigorously tested and do not guarantee feasibility or stability for use in production environments. {/alert} General Approach Roughly divided into several threads, used for: Logging Target application instance management (potentially more than one thread) Listening for IPC messages Processing received IPC messages (main process) This article focuses on the logging part. Design Rationale Why dedicate a separate thread to logging? My consideration is that since it's inherently a multi-threaded architecture, a unified logging module is necessary. If each thread prints independently, it's highly likely that two threads could write to the file or output to the console simultaneously, causing log chaos. Therefore, the general idea for logging is: Define a queue to store log content and level. Create a thread that continuously takes elements from the queue, deciding whether to print to the console or output to a file based on the set log level. External components push log content to the queue. Some Detailed Considerations Ensure portability by using the STL library as much as possible, e.g., using std::thread instead of pthread. Ensure thread safety, requiring protection of relevant variables with mutexes or similar mechanisms. Make the thread wait when the log queue is empty; thought of writing a blocking queue similar to Java's BlockingQueue. Specify a log level; only logs with a level meeting or exceeding this threshold will be saved or printed. Implement variadic arguments via va_list to give the logging function a usage experience similar to sprintf. Start Coding With the above approach, the overall coding becomes quite simple. BlockingQueue Got lazy here, let DeepSeek write this part directly To implement a multi-thread-safe blocking queue where calling front() blocks until another thread adds an element, we can combine a mutex (std::mutex) and a condition variable (std::condition_variable) to synchronize thread operations. Code Implementation Mutex (std::mutex) All operations on the queue (push、front、pop、empty) need to acquire the lock first, ensuring only one thread can modify the queue at a time and avoiding data races. Condition Variable (std::condition_variable) When front() is called and the queue is empty, the thread releases the lock and blocks via cv_.wait(), until another thread calls push() to add an element and wakes up one waiting thread via cv_.notify_one(). cv_.wait() needs to be used with std::unique_lock and automatically releases the lock while waiting to avoid deadlocks. Uses a predicate check ([this] { return !queue_.empty(); }) to prevent spurious wakeups. Element Retrieval and Removal front() returns a copy of the front element (not a reference), ensuring the caller gets the data after the queue's lock is released, avoiding dangling references. pop() must be called explicitly to remove the element, ensuring controllable queue state. #include <queue> // queue #include <mutex> // mutex #include <condition_variable> // condition_variable template<typename T> class BlockingQueue { public: // Add an element to the queue void push(const T& item) { std::lock_guard<std::mutex> lock(mtx_); queue_.push(item); cv_.notify_one(); // Notify one waiting thread } // Get the front element (blocks until queue is not empty) T front() { std::unique_lock<std::mutex> lock(mtx_); cv_.wait(lock, [this] { return !queue_.empty(); }); // Block until queue not empty return queue_.front(); } // Get and remove the front element T take() { std::unique_lock<std::mutex> lock(mtx_); cv_.wait(lock, [this] { return !queue_.empty(); }); T item = std::move(queue_.front()); // Use move semantics to avoid copy queue_.pop(); return item; } // Remove the front element (requires external call, non-blocking) void pop() { std::lock_guard<std::mutex> lock(mtx_); if (!queue_.empty()) { queue_.pop(); } } // Check if the queue is empty bool empty() const { std::lock_guard<std::mutex> lock(mtx_); return queue_.empty(); } private: mutable std::mutex mtx_; // Mutex std::condition_variable cv_; // Condition variable std::queue<T> queue_; // Internal queue }; Log Class Log.h #pragma once #include <iostream> #include <fstream> #include <cstring> #include <thread> #include <chrono> #include <mutex> #include <cstdio> #include <cstdarg> #include <atomic> #include "BlockingQueue.h" enum LogLevel { LEVEL_VERBOSE,LEVEL_INFO,LEVEL_WARN,LEVEL_ERROR,LEVEL_FATAL,LEVEL_OFF }; struct LogMsg { short m_LogLevel; std::string m_strTimestamp; std::string m_strLogMsg; }; class Log { private: std::ofstream m_ofLogFile; // Log file output stream std::mutex m_lockFile; // File operation mutex std::thread m_threadMain; // Background log processing thread BlockingQueue<LogMsg> m_msgQueue; // Thread-safe blocking queue short m_levelLog, m_levelPrint; // File and console log level thresholds std::atomic<bool> m_exit_requested{ false }; // Thread exit flag std::string getTime(); // Get current timestamp std::string level2str(short level, bool character_only); // Level to string void logThread(); // Background thread function public: Log(short default_loglevel = LEVEL_WARN, short default_printlevel = LEVEL_INFO); ~Log(); void push(short level, const char* msg, ...); // Add log (supports formatting) void set_level(short loglevel, short printlevel); // Set log levels bool open(std::string filename); // Open log file bool close(); // Close log file }; Log.cpp #include "Log.h" std::string Log::getTime() { using sc = std::chrono::system_clock; std::time_t t = sc::to_time_t(sc::now()); char buf[20]; #ifdef _WIN32 std::tm timeinfo; localtime_s(&timeinfo,&t); sprintf_s(buf, "%04d.%02d.%02d-%02d:%02d:%02d", timeinfo.tm_year + 1900, timeinfo.tm_mon + 1, timeinfo.tm_mday, timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec ); #else strftime(buf, 20, "%Y.%m.%d-%H:%M:%S", localtime(&t)); #endif return buf; } std::string Log::level2str(short level, bool character_only) { switch (level) { case LEVEL_VERBOSE: return character_only ? "V" : "Verbose"; case LEVEL_WARN: return character_only ? "W" : "Warning"; case LEVEL_ERROR: return character_only ? "E" : "Error"; case LEVEL_FATAL: return character_only ? "F" : "Fatal"; } return character_only ? "I" : "Info"; } void Log::logThread() { while (true) { LogMsg front = m_msgQueue.take(); // Block until a message arrives // Handle file writing if (front.m_LogLevel >= m_levelLog) { std::lock_guard<std::mutex> lock(m_lockFile); // RAII manage lock if (m_ofLogFile) { m_ofLogFile << front.m_strTimestamp << ' ' << level2str(front.m_LogLevel, true) << ": " << front.m_strLogMsg << std::endl; } } // Handle console printing if (front.m_LogLevel >= m_levelPrint) { printf("%s %s: %s\n", front.m_strTimestamp.c_str(), level2str(front.m_LogLevel, true).c_str(), front.m_strLogMsg.c_str()); } // Check exit condition: queue is empty and flag is true if (m_exit_requested.load() && m_msgQueue.empty()) break; } return; } Log::Log(short default_loglevel, short default_printlevel) { set_level(default_loglevel, default_printlevel); m_threadMain = std::thread(&Log::logThread, this); } Log::~Log() { m_exit_requested.store(true); m_msgQueue.push({ LEVEL_INFO, getTime(), "Exit." }); // Wake potentially blocked thread if (m_threadMain.joinable()) m_threadMain.join(); close(); // Ensure file is closed } void Log::push(short level, const char* msg, ...) { va_list args; va_start(args, msg); const int len = vsnprintf(nullptr, 0, msg, args); va_end(args); if (len < 0) return; std::vector<char> buf(len + 1); va_start(args, msg); vsnprintf(buf.data(), buf.size(), msg, args); va_end(args); m_msgQueue.push({level,getTime(),buf.data()}); } void Log::set_level(short loglevel, short printlevel) { m_levelLog = loglevel; m_levelPrint = printlevel; } bool Log::open(std::string filename) { m_lockFile.lock(); m_ofLogFile.open(filename.c_str(), std::ios::out); m_lockFile.unlock(); return (bool)m_ofLogFile; } bool Log::close() { m_lockFile.lock(); m_ofLogFile.close(); m_lockFile.unlock(); return false; } Explanation Class/Structure Explanation LogLevel Enum Defines log levels: VERBOSE, INFO, WARN, ERROR, FATAL, OFF。 OFF should not be used as a level for recorded logs, only for setting the threshold when needing to disable all logging. LogMsg Struct Encapsulates a log message: m_LogLevel: The log level. m_strTimestamp: Timestamp string. m_strLogMsg: The log content. Member Variable Explanation Variable Explanation m_ofLogFile File output stream for writing to the log file. m_lockFile Mutex protecting file operations. m_threadMain Background thread handling consumption of log messages. m_msgQueue Blocking queue storing pending log messages. m_levelLog Minimum log level for writing to file (messages with level >= this are recorded). m_levelPrint Minimum log level for printing to console. m_exit_requested Atomic flag controlling log thread exit. Function Explanation Function Explanation getTime Gets the current timestamp string (cross-platform implementation). level2str Converts log level to string (e.g., LEVEL_INFO → "I" or "Info"). logThread Background thread function: consumes queue messages, writes to file or prints. Constructor Initializes log levels, starts the background thread. Destructor Sets exit flag, waits for thread to finish, ensures remaining messages are processed. push Formats log message (supports variadic arguments) and pushes to the queue. set_level Dynamically sets the log and print levels. open/close Opens/closes the log file. Complete code and test sample download: demo.zip
Querying DNS Records Using WINAPI in C++
This article has been migrated from an old blog (a kind of cyber reincarnation). Corrections are welcome if any errors are found. Timeline {timeline} {timeline-item color="#50BFFF"} July 5, 2022: Published on the old blog, available on archive.org {/timeline-item} {timeline-item color="#50BFFF"} March 20, 2025: Migrated to the current blog {/timeline-item} {timeline-item color="#4F9E28"} Febuary 21, 2026: Added reminder for header and static library file links {/timeline-item} {/timeline} Background Multiple access endpoints were configured for an API to handle service unavailability in certain regions where access might be blocked. Initially, I considered creating a file within the website to store node information, but this approach proved unfeasible because regions with connectivity issues couldn't retrieve information about other endpoints. Then, the idea emerged to use DNS resolution records - specifically, a TXT record - to store the relevant node data for querying. Implementation Research After searching on Bing, I found that most existing articles use sockets to directly send query packets. However, these methods typically only retrieve A and CNAME records, which wasn't suitable for my needs. Finally, I focused on the DnsQuery function mentioned in MSDN and found a sample article: Use DnsQuery to Resolve Host Names. Function Analysis According to the official documentation, the function requires the header windns.h and needs to link against the libraries Ws2_32.lib and Dnsapi.lib. The function parameters are as follows: DNS_STATUS DnsQuery_A( [in] PCSTR pszName, [in] WORD wType, [in] DWORD Options, [in, out, optional] PVOID pExtra, [out, optional] PDNS_RECORD *ppQueryResults, [out, optional] PVOID *pReserved ); Parameter Explanation: pszName is the hostname to query. wType is the query type, such as A record, CNAME, TXT, etc. Specific constants are documented by MSDN: DNS Constants. Options literally means the query method. I used DNS_QUERY_STANDARD directly. Details can also be found in the documentation: DNS Constants. pExtra and pReserved can simply be set to NULL. ppQueryResultsis a pointer to the query results, of type PDNS_RECORD*. This requires passing a reference to the variable's value. Return value: The DNS_STATUS type. If the query fails, it returns the corresponding error code from Winerror.h. Usage Understanding the usage method made it straightforward. To avoid linker errors, make sure to include the following headers and libraries at the beginning: #include <windns.h> #pragma comment(lib, "Ws2_32.lib") #pragma comment(lib, "Dnsapi.lib") My TXT record domain is test.iyoroy.cn. The query code is as follows: PDNS_RECORD pDnsRecord; DNS_STATUS QueryRet = DnsQuery(L"test.iyoroy.cn", DNS_TYPE_TEXT, DNS_QUERY_STANDARD, NULL, &pDnsRecord, NULL); if (QueryRet) { MessageBox(GhWnd, L"DNS query failed!\r\nWill use the default node.", L"Warning", MB_ICONERROR | MB_OK); } std::wstring strQueryRes = *pDnsRecord->Data.TXT.pStringArray; // This is the query result. Successfully implemented the functionality. Postscript TXT records automatically filter out spaces and line breaks. Therefore, I chose to store Base64 encoded data in the record, decode it when used, and then split it using stringstream. For Base64 decoding, I adapted the code from this article: Base64 Encoding and Decoding in C++. Below is the rewritten decoding function using wide characters: {collapse} {collapse-item label="Expand Code"} //Function: Base64 Decryption static const std::wstring base64_chars = L"ABCDEFGHIJKLMNOPQRSTUVWXYZ" L"abcdefghijklmnopqrstuvwxyz" L"0123456789+/"; static inline bool is_base64(wchar_t c) { return (isalnum(c) || (c == L'+') || (c == L'/')); } std::wstring base64_decode(std::wstring const& encoded_string) { int in_len = encoded_string.size(); int i = 0; int j = 0; int in_ = 0; wchar_t char_array_4[4], char_array_3[3]; std::wstring ret; while (in_len-- && (encoded_string[in_] != L'=') && is_base64(encoded_string[in_])) { char_array_4[i++] = encoded_string[in_]; in_++; if (i == 4) { for (i = 0; i < 4; i++) char_array_4[i] = base64_chars.find(char_array_4[i]); char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4); char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2); char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3]; for (i = 0; (i < 3); i++) ret += char_array_3[i]; i = 0; } } if (i) { for (j = i; j < 4; j++) char_array_4[j] = 0; for (j = 0; j < 4; j++) char_array_4[j] = base64_chars.find(char_array_4[j]); char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4); char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2); char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3]; for (j = 0; (j < i - 1); j++) ret += char_array_3[j]; } return ret; } Splitting the decoded string: wstringstream wDnsRec(base64_decode(*pDnsRecord->Data.TXT.pStringArray)); wstring wNodes[16]; unsigned int nNodes = 0; while (wDnsRec >> wNodes[nNodes]) nNodes++; {/collapse-item} {/collapse} After that, selecting an available node becomes possible. Reference Articles: https://learn.microsoft.com/en-us/previous-versions/troubleshoot/windows/win32/use-dnsquery-resolve-host-names https://learn.microsoft.com/en-us/windows/win32/dns/dns-constants https://blog.csdn.net/sky04/article/details/6881649
Using Infrared Camera for Face Recognition on Xiaomi 845 Series with Custom ROMs
Background The Xiaomi 845 series, specifically the Mi 8 and Mi 8 Pro (UDFPS), feature a dedicated front-facing infrared camera for facial recognition. This enables facial unlock functionality even in complete darkness. However, when using a custom ROM like LineageOS compiled from its standard device tree, the infrared camera remains inaccessible, forcing the system to rely on the standard front camera. Interestingly, PixelExperience ROMs successfully utilize this hardware. This article explores the reasons behind this discrepancy and outlines the solution. The Cause CameraID The first step is directing the face recognition module to use the infrared camera. Examining the PixelExperience device tree source code reveals an overlay that configures the face unlock service to use the camera with ID 5. https://github.com/PixelExperience-Devices/device_xiaomi_dipper/blob/fourteen/overlay/packages/apps/FaceUnlockService/app/src/main/res/values/config.xml <?xml version="1.0" encoding="utf-8"?> <resources> <integer name="override_front_cam_id">5</integer> <bool name="use_alternative_vendor_impl">true</bool> </resources> PixelExperience uses Motorola's face unlock solution. Many other custom ROMs, however, utilize AOSPA's ParanoidSense. Therefore, the PixelExperience overlay method isn't directly applicable. Inspecting the ParanoidSense source code shows that it uses a system property, ro.face.sense_service.camera_id, to identify which camera to use. https://github.com/AOSPA/android_packages_apps_ParanoidSense/blob/uvite/src/co/aospa/sense/camera/CameraUtil.kt#L32 val cameraIdProp = SystemProperties.get("ro.face.sense_service.camera_id") Thus, in theory, simply setting the ro.face.sense_service.camera_id property to 5 should direct it to the infrared camera. The vendor.camera.aux.packagelist Property Merely setting the CameraID property is insufficient; the face unlock module would simply fail to access the camera. Delving into the Android framework code reveals that the system, by default, hides auxiliary cameras (AuxCameras) beyond the primary front and rear cameras. The infrared camera falls into this AuxCamera category. Using LineageOS framework code as an example: https://github.com/LineageOS/android_frameworks_base/blob/lineage-21.0/core/java/android/hardware/Camera.java#L295-L301 /** * Returns the number of physical cameras available on this device. * The return value of this method might change dynamically if the device * supports external cameras and an external camera is connected or * disconnected. * * If there is a * {@link android.hardware.camera2.CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA * logical multi-camera} in the system, to maintain app backward compatibility, this method will * only expose one camera per facing for all logical camera and physical camera groups. * Use camera2 API to see all cameras. * * @return total number of accessible camera devices, or 0 if there are no * cameras or an error was encountered enumerating them. */ public static int getNumberOfCameras() { int numberOfCameras = _getNumberOfCameras(); if (!shouldExposeAuxCamera() && numberOfCameras > 2) { numberOfCameras = 2; } return numberOfCameras; } As shown, even if a device has more than two cameras, if the shouldExposeAuxCamera() function returns False, the system reports only two cameras, preventing access to any camera with an ID greater than 1. Let's examine this function: https://github.com/LineageOS/android_frameworks_base/blob/lineage-21.0/core/java/android/hardware/Camera.java#L264-L278 /** * @hide */ public static boolean shouldExposeAuxCamera() { /** * Force to expose only two cameras * if the package name does not falls in this bucket */ String packageName = ActivityThread.currentOpPackageName(); if (packageName == null) return true; List<String> packageList = Arrays.asList( SystemProperties.get("vendor.camera.aux.packagelist", packageName).split(",")); List<String> packageExcludelist = Arrays.asList( SystemProperties.get("vendor.camera.aux.packageexcludelist", "").split(",")); return packageList.contains(packageName) && !packageExcludelist.contains(packageName); } The logic is clear: if the current application's package name is listed in the vendor.camera.aux.packagelist system property and is not in the vendor.camera.aux.packageexcludelist property, the function returns True, allowing access to auxiliary cameras. The solution becomes evident: add the ParanoidSense package name (co.aospa.sense) to the vendor.camera.aux.packagelist property. The Solution Add the ParanoidSense package name co.aospa.sense to the vendor.camera.aux.packagelist property: https://github.com/YoriInstitute/crDroidAndroid_device_xiaomi_sdm845-common/commit/28fe5bc41d49b31b4acb840bf167b70d70a40c61 Set the ro.face.sense_service.camera_id property to specify the correct camera ID: https://github.com/YoriInstitute/crDroidAndroid_device_xiaomi_equuleus/commit/67518f130650e4592b5f4c7210248072058d48cc https://github.com/YoriInstitute/AOSPA_device_xiaomi_equuleus/blob/topaz/device.mk#L397-L399 In the crDroid-modified version of ParanoidSense, which is placed in the system_ext partition (refer: https://gitlab.com/crdroidandroid/android_packages_apps_FaceUnlock/-/commit/545688260eb32ba19f348e84e3cae89ba29f20d1), this property should be added to the corresponding system_ext prop file.
Adding KernelSU Support to Android 4.9 Kernel
Introduction KernelSU has been available for a while. Officially, it only supports GKI kernels, but the project provides integration guidelines for non-GKI kernels. The Xiaomi Mi 8 Pro, which I use, is based on the Android 4.9 kernel. Testing showed that directly using KernelSU's integration script caused issues, so I'm documenting the process here. Special thanks to: Developer Tomsec for providing the 4.9 kernel fix. Prerequisites Android Kernel source code. Here, we use LineageOS/android_kernel_xiaomi_sdm845 Network access to GitHub AnyKernel3 source code. You can refer to this repository: YoriInstitute/AnyKernel3-equuleus Cross-compiler. You can refer to this repository: https://github.com/YoriInstitute/AnyKernel3-equuleus/blob/13/.github/workflows/build.yml#L21-L23 A build server Let's Start Configuring the build environment won't be detailed here. You can integrate it directly into the boot.img when building the ROM, or compile it separately as an AnyKernel3 flashable zip. This guide uses the AnyKernel3 approach. Download Kernel Source Code and Cross-Compiler git clone https://github.com/KaguraiYoRoy/android_kernel_xiaomi_sdm845 -b lineage-20 sdm845 #Kernel source git clone https://android.googlesource.com/platform/prebuilts/clang/host/linux-x86 --depth=1 -b android-13.0.0_r43 clang #Clang git clone https://android.googlesource.com/platform/prebuilts/gcc/linux-x86/aarch64/aarch64-linux-android-4.9 -b android-10.0.0_r32 --depth=1 #aarch64 GCC git clone https://android.googlesource.com/platform/prebuilts/gcc/linux-x86/arm/arm-linux-androideabi-4.9 -b android-10.0.0_r32 --depth=1 #arm GCC Different kernels might require different cross-compilers. Please adjust according to your specific case. {alert type="info"} When cloning the kernel source, --depth=1 wasn't used initially to facilitate easier cherry-picking later. If you plan to manually modify the kernel source instead of using cherry-pick, you can add this parameter to significantly reduce download time. {/alert} Modify Kernel Source Refer to the following two commits: https://github.com/OnlyTomInSecond/android_kernel_xiaomi_sdm845/commit/7862fd2c14b69a1a346b7c699f8370e2de423eef commit b5853fb7cefdc8a2e160cb73512dc6b51569fa66 Author: OnlyTomInSecond <q2781273965@gmail.com> Date: Wed Apr 5 11:05:12 2023 +0800 kernelsu: allow init exec ksud under nosuid Change-Id: I8aa6e6d3cbee1addd5da9bb48b4c08ce91f9db81 diff --git a/security/selinux/hooks.c b/security/selinux/hooks.c index 4abba0e1674d..693f2ac03352 100644 --- a/security/selinux/hooks.c +++ b/security/selinux/hooks.c @@ -2320,7 +2320,10 @@ static int check_nnp_nosuid(const struct linux_binprm *bprm, { int nnp = (bprm->unsafe & LSM_UNSAFE_NO_NEW_PRIVS); int nosuid = !mnt_may_suid(bprm->file->f_path.mnt); - int rc; + int rc, error; + static u32 ksu_sid; + char *secdata; + u32 seclen; if (!nnp && !nosuid) return 0; /* neither NNP nor nosuid */ @@ -2328,6 +2331,18 @@ static int check_nnp_nosuid(const struct linux_binprm *bprm, if (new_tsec->sid == old_tsec->sid) return 0; /* No change in credentials */ + if(!ksu_sid){ + security_secctx_to_secid("u:r:su:s0", strlen("u:r:su:s0"), &ksu_sid); + } + error = security_secid_to_secctx(old_tsec->sid, &secdata, &seclen); + if (!error) { + rc = strcmp("u:r:init:s0",secdata); + security_release_secctx(secdata, seclen); + if(rc == 0 && new_tsec->sid == ksu_sid){ + return 0; + } + } + /* * The only transitions we permit under NNP or nosuid * are transitions to bounded SIDs, i.e. SIDs that are https://github.com/OnlyTomInSecond/android_kernel_xiaomi_sdm845/commit/81d7168f2f01e6aba0932a4787119fbc541eba58 commit 8bfe4d4de25650505798cba0a5a20db4b2ab7dd6 Author: OnlyTomInSecond <q2781273965@gmail.com> Date: Thu Oct 19 17:55:23 2023 +0800 Kernelsu: add path_umount implementation. Change-Id: I3b0273e9857c51cc3215afab0d2cebe0dff38cfb diff --git a/fs/namespace.c b/fs/namespace.c index 21fd423b19cf..219a501337b0 100644 --- a/fs/namespace.c +++ b/fs/namespace.c @@ -1711,6 +1711,38 @@ static inline bool may_mandlock(void) } #endif +static int can_umount(const struct path *path, int flags) +{ + struct mount *mnt = real_mount(path->mnt); + + if (!may_mount()) + return -EPERM; + if (path->dentry != path->mnt->mnt_root) + return -EINVAL; + if (!check_mnt(mnt)) + return -EINVAL; + if (mnt->mnt.mnt_flags & MNT_LOCKED) /* Check optimistically */ + return -EINVAL; + if (flags & MNT_FORCE && !capable(CAP_SYS_ADMIN)) + return -EPERM; + return 0; +} + +// caller is responsible for flags being sane +int path_umount(struct path *path, int flags) +{ + struct mount *mnt = real_mount(path->mnt); + int ret; + + ret = can_umount(path, flags); + if (!ret) + ret = do_umount(mnt, flags); + + /* we mustn't call path_put() as that would clear mnt_expiry_mark */ + dput(path->dentry); + mntput_no_expire(mnt); + return ret; +} /* * Now umount can handle mount points as well as block devices. * This is important for filesystems which use unnamed block devices. Solution Source: Tomsec Enable KPROBE Support Open your kernel's defconfig file and add the following three lines: CONFIG_KPROBES=y CONFIG_HAVE_KPROBES=y CONFIG_KPROBE_EVENTS=y Integrate KernelSU This step is the same as the official ksu guide. Execute the following command in the kernel root directory: curl -LSs "https://raw.githubusercontent.com/tiann/KernelSU/main/kernel/setup.sh" | bash -s v0.9.5 {alert type="warning"} Note: The highest version of KernelSU supporting non-GKI kernels is v0.9.5. Do not use a higher version tag, or the build will fail. {/alert} Write the AnyKernel3 Script Download AnyKernel3 and modify anykernel.sh according to the README. You can refer to my repository for modifications: Repository: https://github.com/iYoRoy-AOSP-Institute/AnyKernel3-equuleus/blob/14/anykernel.sh {collapse} {collapse-item label="Expand Code"} commit 3b46e03e0ffdd52bbb3f51b45b0ff649a407d1a5 Author: KaguraiYoRoy <3363384229@qq.com> Date: Sat Sep 9 02:41:54 2023 +0000 anykernel.sh: modify for equuleus diff --git a/anykernel.sh b/anykernel.sh old mode 100755 new mode 100644 index 367d29f..c950ce7 --- a/anykernel.sh +++ b/anykernel.sh @@ -4,23 +4,17 @@ ### AnyKernel setup # global properties properties() { ' -kernel.string=ExampleKernel by osm0sis @ xda-developers +kernel.string=Xiaomi 8 Pro Kernel by Kagura iYoRoy @ iYoRoy Studio do.devicecheck=1 do.modules=0 -do.systemless=1 +do.systemless=0 do.cleanup=1 do.cleanuponabort=0 -device.name1=maguro -device.name2=toro -device.name3=toroplus -device.name4=tuna -device.name5= -supported.versions= +device.name1=equuleus +supported.versions=13 supported.patchlevels= -supported.vendorpatchlevels= '; } # end properties - ### AnyKernel install ## boot files attributes boot_attributes() { @@ -29,7 +23,7 @@ set_perm_recursive 0 0 750 750 $ramdisk/init* $ramdisk/sbin; } # end attributes # boot shell variables -block=/dev/block/platform/omap/omap_hsmmc.0/by-name/boot; +block=/dev/block/bootdevice/by-name/boot; is_slot_device=0; ramdisk_compression=auto; patch_vbmeta_flag=auto; @@ -41,32 +35,26 @@ patch_vbmeta_flag=auto; dump_boot; # use split_boot to skip ramdisk unpack, e.g. for devices with init_boot ramdisk # init.rc -backup_file init.rc; -replace_string init.rc "cpuctl cpu,timer_slack" "mount cgroup none /dev/cpuctl cpu" "mount cgroup none /dev/cpuctl cpu,timer_slack"; +#backup_file init.rc; +#replace_string init.rc "cpuctl cpu,timer_slack" "mount cgroup none /dev/cpuctl cpu" "mount cgroup none /dev/cpuctl cpu,timer_slack"; # init.tuna.rc -backup_file init.tuna.rc; -insert_line init.tuna.rc "nodiratime barrier=0" after "mount_all /fstab.tuna" "\tmount ext4 /dev/block/platform/omap/omap_hsmmc.0/by-name/userdata /data remount nosuid nodev noatime nodiratime barrier=0"; -append_file init.tuna.rc "bootscript" init.tuna; +#backup_file init.tuna.rc; +#insert_line init.tuna.rc "nodiratime barrier=0" after "mount_all /fstab.tuna" "\tmount ext4 /dev/block/platform/omap/omap_hsmmc.0/by-name/userdata /data remount nosuid nodev noatime nodiratime barrier=0"; +#append_file init.tuna.rc "bootscript" init.tuna; # fstab.tuna -backup_file fstab.tuna; -patch_fstab fstab.tuna /system ext4 options "noatime,barrier=1" "noatime,nodiratime,barrier=0"; -patch_fstab fstab.tuna /cache ext4 options "barrier=1" "barrier=0,nomblk_io_submit"; -patch_fstab fstab.tuna /data ext4 options "data=ordered" "nomblk_io_submit,data=writeback"; -append_file fstab.tuna "usbdisk" fstab; +#backup_file fstab.tuna; +#patch_fstab fstab.tuna /system ext4 options "noatime,barrier=1" "noatime,nodiratime,barrier=0"; +#patch_fstab fstab.tuna /cache ext4 options "barrier=1" "barrier=0,nomblk_io_submit"; +#patch_fstab fstab.tuna /data ext4 options "data=ordered" "nomblk_io_submit,data=writeback"; +#append_file fstab.tuna "usbdisk" fstab; write_boot; # use flash_boot to skip ramdisk repack, e.g. for devices with init_boot ramdisk ## end boot install -## init_boot files attributes -#init_boot_attributes() { -#set_perm_recursive 0 0 755 644 $ramdisk/*; -#set_perm_recursive 0 0 750 750 $ramdisk/init* $ramdisk/sbin; -#} # end attributes - -# init_boot shell variables +## init_boot shell variables #block=init_boot; #is_slot_device=1; #ramdisk_compression=auto; @@ -98,13 +86,7 @@ write_boot; # use flash_boot to skip ramdisk repack, e.g. for devices with init_ ## end vendor_kernel_boot install -## vendor_boot files attributes -#vendor_boot_attributes() { -#set_perm_recursive 0 0 755 644 $ramdisk/*; -#set_perm_recursive 0 0 750 750 $ramdisk/init* $ramdisk/sbin; -#} # end attributes - -# vendor_boot shell variables +## vendor_boot shell variables #block=vendor_boot; #is_slot_device=1; #ramdisk_compression=auto; @@ -118,4 +100,3 @@ write_boot; # use flash_boot to skip ramdisk repack, e.g. for devices with init_ #write_boot; # use flash_boot to skip ramdisk repack, e.g. for dtb on devices with hdr v4 but no vendor_kernel_boot ## end vendor_boot install - {/collapse-item} {/collapse} Compile the Kernel It's recommended to refer to online tutorials for this step, as compilation parameter settings can vary for different kernels. I've packaged the compilation process into a shell script for reference: {alert type="info"} Please replace $WORKSPACE below with your own working directory. {/alert} #!/bin/bash ARCH="arm64" CLANG_DIR="$WORKSPACE/clang/clang-r450784d" #Clang directory CC="$CLANG_DIR/bin/clang" export PATH="$CLANG_DIR/bin:$PATH" OUT_DIR="./out" CLANG_TRIPLE="aarch64-linux-gnu-" CROSS_COMPILE="$WORKSPACE/aarch64-linux-android-4.9/bin/aarch64-linux-androidkernel-" #aarch64 GCC CROSS_COMPILE_ARM32="$WORKSPACE/arm-linux-androideabi-4.9/bin/arm-linux-androidkernel-" #arm GCC CC_ADDITION_FLAGS="AR=llvm-ar NM=llvm-nm OBJCOPY=llvm-objcopy OBJDUMP=llvm-objdump STRIP=llvm-strip LLVM_IAS=1 LLVM=1 LD=ld.lld" ANYKERNEL_DIR="$WORKSPACE/AnyKernel3" #Location of your prepared AnyKernel3 directory THREAD=$(nproc --all) args="-j$THREAD O=$OUT_DIR ARCH=$ARCH CROSS_COMPILE=$CROSS_COMPILE CROSS_COMPILE_ARM32=$CROSS_COMPILE_ARM32 CC=$CC CLANG_TRIPLE=$CLANG_TRIPLE $CC_ADDITION_FLAGS" ### Compile Kernel ### echo "[+]Args: $args" echo "[+}Generate .config" make equuleus_user_defconfig $args #Here 'equuleus_user_defconfig' is the defconfig file for the equuleus kernel. Replace it with your kernel's config file name. echo "[+]Begin Build" make $args ### Compilation Complete ### ### Create AnyKernel3 Zip ### cd $WORKSPACE/sdm845/KernelSU KSU_VERSION='v0.9.5' TARGET_KERNEL="Mi8Pro-LineageKernel-KernelSU$KSU_VERSION" #Kernel filename echo "[+]KernelSU Version: $KSU_VERSION" echo "[+]Target file: $TARGET_KERNEL" cd $WORKSPACE/AnyKernel3 cp $WORKSPACE/sdm845/out/arch/arm64/boot/Image . cp $WORKSPACE/sdm845/out/arch/arm64/boot/Image.gz . cp $WORKSPACE/sdm845/out/arch/arm64/boot/Image.gz-dtb . OUTPUT_FILE=${TARGET_KERNEL}-$(date +"%y.%m.%d").zip echo "[+]Output: $OUTPUT_FILE" zip -r $OUTPUT_FILE * mv $OUTPUT_FILE $WORKSPACE Flashing Use a recovery like TWRP to flash the zip. Automatically Compile the Kernel using GitHub Actions Refer to this YAML file for workflow setup: https://github.com/iYoRoy-AOSP-Institute/AnyKernel3-equuleus/blob/13/.github/workflows/build.yml {collapse} {collapse-item label="Expand Code"} name: Build AnyKernel on: push: schedule: - cron: '0 0 * * 0' jobs: build: runs-on: ubuntu-latest steps: - name: Download run: | echo "Free space:" df -h cd $GITHUB_WORKSPACE echo "[+]Install packages:" sudo apt update sudo apt install zip bc bison build-essential ccache curl flex g++-multilib gcc-multilib git gnupg gperf imagemagick lib32ncurses5-dev lib32readline-dev lib32z1-dev liblz4-tool libncurses5-dev libncurses5 libsdl1.2-dev libssl-dev libwxgtk3.0-gtk3-dev libxml2 libxml2-utils lzop pngcrush rsync schedtool squashfs-tools xsltproc zip zlib1g-dev make unzip python-is-python3 echo "[+]Clone dependencies:" git clone https://github.com/KaguraiYoRoy/android_kernel_xiaomi_sdm845 -b lineage-20 sdm845 git clone https://android.googlesource.com/platform/prebuilts/clang/host/linux-x86 --depth=1 -b android-13.0.0_r43 clang git clone https://android.googlesource.com/platform/prebuilts/gcc/linux-x86/aarch64/aarch64-linux-android-4.9 -b android-10.0.0_r32 --depth=1 git clone https://android.googlesource.com/platform/prebuilts/gcc/linux-x86/arm/arm-linux-androideabi-4.9 -b android-10.0.0_r32 --depth=1 git clone https://github.com/KaguraiYoRoy/AnyKernel3-equuleus AnyKernel3 --depth=1 - name: Setup KernelSU run: | cd $GITHUB_WORKSPACE/sdm845 curl -LSs "https://raw.githubusercontent.com/tiann/KernelSU/main/kernel/setup.sh" | bash -s main - name: Build Kernel run: | cd $GITHUB_WORKSPACE/sdm845 ARCH="arm64" CLANG_DIR="$GITHUB_WORKSPACE/clang/clang-r450784d" CC="$CLANG_DIR/bin/clang" export PATH="$CLANG_DIR/bin:$PATH" OUT_DIR="./out" CLANG_TRIPLE="aarch64-linux-gnu-" CROSS_COMPILE="$GITHUB_WORKSPACE/aarch64-linux-android-4.9/bin/aarch64-linux-androidkernel-" CROSS_COMPILE_ARM32="$GITHUB_WORKSPACE/arm-linux-androideabi-4.9/bin/arm-linux-androidkernel-" CC_ADDITION_FLAGS="AR=llvm-ar NM=llvm-nm OBJCOPY=llvm-objcopy OBJDUMP=llvm-objdump STRIP=llvm-strip LLVM_IAS=1 LLVM=1 LD=ld.lld" THREAD=$(nproc --all) ANYKERNEL_DIR="$GITHUB_WORKSPACE/AnyKernel3" args="-j$THREAD O=$OUT_DIR ARCH=$ARCH CROSS_COMPILE=$CROSS_COMPILE CROSS_COMPILE_ARM32=$CROSS_COMPILE_ARM32 CC=$CC CLANG_TRIPLE=$CLANG_TRIPLE $CC_ADDITION_FLAGS" echo "[+]Args: $args" echo "[+}Generate .config" make equuleus_user_defconfig $args echo "[+]Begin Build" make $args - name: Compress run: | cd $GITHUB_WORKSPACE/sdm845/KernelSU KSU_VERSION=`expr 10000 + \`git rev-list --count HEAD\` + 200` TARGET_KERNEL="Mi8Pro-LineageKernel-KernelSU$KSU_VERSION" echo "[+]KernelSU Version: $KSU_VERSION" echo "[+]Target file: $TARGET_KERNEL" cd $GITHUB_WORKSPACE/AnyKernel3 cp $GITHUB_WORKSPACE/sdm845/out/arch/arm64/boot/Image . cp $GITHUB_WORKSPACE/sdm845/out/arch/arm64/boot/Image.gz . cp $GITHUB_WORKSPACE/sdm845/out/arch/arm64/boot/Image.gz-dtb . OUTPUT_FILE=${TARGET_KERNEL}-$(date +"%y.%m.%d").zip echo "[+]Output: $OUTPUT_FILE" zip -r $OUTPUT_FILE * mv $OUTPUT_FILE $GITHUB_WORKSPACE - name: Release uses: softprops/action-gh-release@v1 with: tag_name: Android13 files: | Mi8Pro*.zip I definitely won't tell you that the script above was also extracted from here x {/collapse-item} {/collapse}
Enabling EROFS Support for an Android ROM with Kernel 4.9
Foreword As everyone knows, I'm a long-standing Mi 8 Pro (Under-Screen Fingerprint Edition) holdout I've always wanted to port erofs to this device (please search online for what erofs is). But after some research, I found out that erofs was merged into the Linux kernel in version 5.10, which means I have to manually integrate it into our 4.9 kernel. Materials Needed Android ROM Source Code: Using LineageOS 22.0 here. Android Kernel Source Code: Using the official LineageOS kernel for Xiaomi SDM845 devices: LineageOS/android_kernel_xiaomi_sdm845 Device Tree: Using the official LineageOS device trees, which include the sdm845-common part: LineageOS/android_device_xiaomi_sdm845-common and the device-specific part: LineageOS/android_device_xiaomi_equuleus A build server {alert type="info"} Note: Since I'm primarily focused on ROM building, this article mainly documents the process within that context. {/alert} Let's Start Setting up the Android ROM build environment won't be covered here, as there are plenty of guides online. Download the ROM source, device tree, and kernel source, and place them in the correct locations. Adding EROFS Support to the Kernel The code changes were picked from ReallySnow's msm-4.9 kernel (Thanks to ReallySnow for the support!). You can refer to these branches: https://github.com/ReallySnow/msm-4.9/tree/erofs https://github.com/YoriInstitute/android_kernel_xiaomi_sdm845/tree/lineage-22.0-erofs Before picking the commits, you need to resolve conflicts in the lz4 compression code. For the detailed relation chain, refer to: https://review.lineageos.org/c/LineageOS/android_kernel_xiaomi_sdm845/+/412614 I tried to submit these changes to LineageOS official, but obviously, they weren't merged x After picking the changes, enable erofs support in the kernel's defconfig file: Open $KERNEL_SOURCE/arch/arm64/configs/vendor/xiaomi/mi845_defconfig and add: CONFIG_EROFS_FS=y: commit 7ae5caab2c15982b3d3821c97e2f2e6126ca1281 (HEAD -> erofs, refs/published/erofs) Author: KaguraiYoRoy <neko@yori.moe> Date: Tue Dec 24 22:49:07 2024 +0800 arch: arm64: configs: mi845: Enable CONFIG_EROFS_FS Change-Id: Ie387b03bde54700d73ccf19f0baf4db62526cc8f diff --git a/arch/arm64/configs/vendor/xiaomi/mi845_defconfig b/arch/arm64/configs/vendor/xiaomi/mi845_defconfig index 7684be360d98..69ea580fdb11 100644 --- a/arch/arm64/configs/vendor/xiaomi/mi845_defconfig +++ b/arch/arm64/configs/vendor/xiaomi/mi845_defconfig @@ -651,3 +651,4 @@ CONFIG_SOUNDWIRE_WCD_CTRL=y CONFIG_WCD9XXX_CODEC_CORE=y CONFIG_WCD_DSP_GLINK=y CONFIG_WCD_SPI_AC=y +CONFIG_EROFS_FS=y \ No newline at end of file Modifying Partition Filesystems and Fstab Open the board configuration file in the device tree and change the filesystem type for the partitions you want to convert to erofs. The example configuration file is located at $WORKING_DIR/device/xiaomi/sdm845-common/BoardConfigCommon.mk: commit 0fce86fb40ca637a8563bef6192afaea1658c881 (HEAD -> erofs, refs/published/erofs) Author: KaguraiYoRoy <neko@yori.moe> Date: Tue Dec 24 23:37:50 2024 +0800 sdm845-common: Switch to erofs Change-Id: I9a8d6de4dcb79cd6bc177451ce3cbc4682748501 diff --git a/BoardConfigCommon.mk b/BoardConfigCommon.mk index cf73d39..700592f 100644 --- a/BoardConfigCommon.mk +++ b/BoardConfigCommon.mk @@ -78,11 +78,11 @@ BOARD_CACHEIMAGE_PARTITION_SIZE := 268435456 BOARD_USERDATAIMAGE_PARTITION_SIZE := 57453555712 BOARD_CACHEIMAGE_FILE_SYSTEM_TYPE := ext4 -BOARD_ODMIMAGE_FILE_SYSTEM_TYPE := ext4 +BOARD_ODMIMAGE_FILE_SYSTEM_TYPE := erofs BOARD_PRODUCTIMAGE_FILE_SYSTEM_TYPE := ext4 BOARD_SYSTEMIMAGE_FILE_SYSTEM_TYPE := ext4 BOARD_SYSTEM_EXTIMAGE_FILE_SYSTEM_TYPE := ext4 -BOARD_VENDORIMAGE_FILE_SYSTEM_TYPE := ext4 +BOARD_VENDORIMAGE_FILE_SYSTEM_TYPE := erofs BOARD_FLASH_BLOCK_SIZE := 262144 # (BOARD_KERNEL_PAGESIZE * 64) Modify the device's fstab file, located here at $WORKING_DIR/device/xiaomi/equuleus/rootdir/fstab.qcom. Change the filesystem type for the relevant partitions to erofs, and reduce the mount parameters to just ro: commit d66bfc8fe722f7368ef56835d35fb88b904e10fb (HEAD -> erofs, refs/published/erofs) Author: KaguraiYoRoy <neko@yori.moe> Date: Tue Dec 24 23:40:08 2024 +0800 equuleus: Update fstab for erofs Change-Id: I814eab1dd77cf2c184ebd398cc4914785bc37ad6 diff --git a/rootdir/etc/fstab.qcom b/rootdir/etc/fstab.qcom index 7aca90a..685ec84 100644 --- a/rootdir/etc/fstab.qcom +++ b/rootdir/etc/fstab.qcom @@ -6,8 +6,8 @@ system /system ext4 ro,barrier=1,discard wait,logical,first_stage_mount system_ext /system_ext ext4 ro,barrier=1,discard wait,logical,first_stage_mount product /product ext4 ro,barrier=1,discard wait,logical,first_stage_mount -vendor /vendor ext4 ro,barrier=1,discard wait,logical,first_stage_mount -odm /odm ext4 ro,barrier=1,discard wait,logical,first_stage_mount +vendor /vendor erofs ro wait,logical,first_stage_mount +odm /odm erofs ro wait,logical,first_stage_mount /dev/block/bootdevice/by-name/userdata /data ext4 noatime,nosuid,nodev,barrier=0,noauto_da_alloc latemount,wait,check,fileencryption=ice,quota /dev/block/bootdevice/by-name/modem /vendor/firmware_mnt vfat ro,shortname=lower,uid=0,gid=1000,dmask=227,fmask=337,context=u:object_r:firmware_file:s0 wait /dev/block/bootdevice/by-name/dsp /vendor/dsp ext4 ro,nosuid,nodev,barrier=1 wait Finally, build and flash the ROM. If all goes well, it should boot into the system. Important Notes LineageOS official has implemented Dynamic Partition for my device model. The device tree modifications described above might not be suitable for your device. erofs is a read-only filesystem. If you plan to flash Gapps later, DO NOT convert the system, system_ext, and product partitions. Frequently Asked Questions Booting into Fastboot mode: Check if the fstab modifications are correct.