Try   HackMD

2025q1 Homework1 (lab0)

contributed by < JeepWay >

作業書寫規範:

  • 無論標題和內文中,中文和英文字元之間要有空白字元 (對排版和文字搜尋有利)
  • 文字訊息 (尤其是程式執行結果) 請避免用圖片來表示,否則不好搜尋和分類
  • 共筆書寫請考慮到日後協作,避免過多的個人色彩,用詞儘量中性
  • 不要在筆記內加入 [TOC] : 筆記左上方已有 Table of Contents (TOC) 功能,不需要畫蛇添足
  • 不要變更預設的 CSS 也不要加入任何佈景主題: 這是「開發紀錄」,用於評分和接受同儕的檢閱
  • 在筆記中貼入程式碼時,避免非必要的行號,也就是該手動將 c=cpp= 變更為 ccpp。行號只在後續討論明確需要行號時,才要出現,否則維持精簡的展現。可留意「你所不知道的 C 語言: linked list 和非連續記憶體」裡頭程式碼展現的方式
  • HackMD 不是讓你張貼完整程式碼的地方,GitHub 才是!因此你在開發紀錄只該列出關鍵程式碼 (善用 diff 標示),可附上對應 GitHub commit 的超連結,列出程式碼是為了「檢討」和「便於他人參與討論」
  • 留意科技詞彙的使用,請參見「資訊科技詞彙翻譯」及「詞彙對照表
  • 不要濫用 :::info, :::success, :::warning 等標示,儘量用清晰的文字書寫。:::danger 則僅限授課教師作為批注使用
  • 避免過多的中英文混用,已有明確翻譯詞彙者,例如「鏈結串列」(linked list) 和「佇列」(queue),就使用該中文詞彙,英文則留給變數名稱、人名,或者缺乏通用翻譯詞彙的場景
  • 在中文敘述中,使用全形標點符號,例如該用「,」,而非 ","。注意書名號的使用,即 ,非「小於」和「大於」符號
  • 避免使用不必要的 emoji 字元

開發環境

$ gcc --version 
gcc (Ubuntu 13.3.0-6ubuntu2~24.04) 13.3.0

$ lscpu
Architecture:             x86_64
  CPU op-mode(s):         32-bit, 64-bit
  Address sizes:          39 bits physical, 48 bits virtual
  Byte Order:             Little Endian
CPU(s):                   16
  On-line CPU(s) list:    0-15
Vendor ID:                GenuineIntel
  Model name:             12th Gen Intel(R) Core(TM) i5-1240P
    CPU family:           6
    Model:                154
    Thread(s) per core:   2
    Core(s) per socket:   12
    Socket(s):            1
    Stepping:             3
    CPU(s) scaling MHz:   16%
    CPU max MHz:          4400.0000
    CPU min MHz:          400.0000
    BogoMIPS:             4224.00
Virtualization features:  
  Virtualization:         VT-x
Caches (sum of all):      
  L1d:                    448 KiB (12 instances)
  L1i:                    640 KiB (12 instances)
  L2:                     9 MiB (6 instances)
  L3:                     12 MiB (1 instance)
NUMA:                     
  NUMA node(s):           1
  NUMA node0 CPU(s):      0-15

開發過程 (queue.c)

開發時間是在看到 commit 1d68fae 且重新 fork 後,這個 commit 主要是跟 Cppcheck 回報 unknownMacro 有關,會觸發該回報的句集是 list_for_each_entrylist_for_each_entry_safe

q_new

Commit 5aa6f25

使用 malloc 來配置記憶體,如果指標 qNULL,代表配置失敗,則回傳 NULL
若配置成功則使用 INIT_LIST_HEAD 函式初始化鏈結串列,然後回傳鏈結串列的 head 的記憶體地址。

q_free

Commit 8c84a05

我在使用 list_for_each_entry 巨集時,make test 有正確執行拿到預期分數,但是在提交程式碼時,會遇到以下錯誤:

Following files need to be cleaned up:
queue.c
Running static analysis...
queue.c:31:5: style: Label 'int' is not used. [unusedLabel]
    list_for_each_entry_safe (entry, safe, head, list)
    ^

Fail to pass static analysis.

這似乎是 Cppcheck 的靜態分析報告,而錯誤源頭正是 commit 1d68fae 新增的程式碼,int 被識別為一個 label 而非類型。為了忽略這種誤判,我參考老師在 queue.c 最上方的註解,在 list_for_each_entry_safe 的前一行添加 /* cppcheck-suppress unusedLabel */,來忽略 Cppcheck 對於 list_for_each_entry_safe 的靜態分析報錯,以順利提交程式碼。

+ /* cppcheck-suppress unusedLabel */
list_for_each_entry_safe (entry, safe, head, list)
    q_release_element(entry);

q_free 函式中,使用 list_for_each_entry_safe 巨集來安全地遍歷鏈結串列中的每一個節點,其中 entry 指標指向當下要被釋放記憶體的節點,safe 指標指向下一個要被處理的節點,指標指向的地址已經由 list_for_each_entry_safe 巨集處理,因此我們不用再去更新指標的內容。

在釋放記憶體時,可使用 queue.h 裡的 q_release_element 函式來完成,以精簡程式碼。

q_insert_head, q_insert_tail

Commit 3ecf781

使用幫手函式 q_new_element 來建立 element_t 物件,在函式裡面使用 harness.hstrdup 巨集 (即 harness.c 中的 test_strdup 函式) 為字串配置新的記憶體空間。使用 strdup 後,要檢查是否成功,如果失敗,要記得釋放掉已經配置好的記憶體,然後回傳 NULL。如果成功則回傳建立的 element_t 物件的記憶體地址。

element_t *q_new_element(char *s)
{
    element_t *e = malloc(sizeof(element_t));
    if (!e)
        return NULL;

    char *tmp = strdup(s);
    if (!tmp) {
        free(e);
        return NULL;
    }
    e->value = tmp;
    return e;
}

element_t 物件建立成功後,q_insert_headq_insert_tail 即可分別透過 list_addlist_add_tail 函式將節點插入到鏈結串列的頭跟尾。

q_remove_head, q_remove_tail

Commit 8316fb7:第一版
Commit 2083449:第二版,修正放入 buffer sp 的字串的長度

因為 q_remove_headq_remove_tail 函式的程式碼有高度重疊性,主要差異於是使用 list_first_entry 還是 list_last_entry 來找到要移除的節點,所以整合成一個函式 q_removeq_remove_headq_remove_tail 透過傳遞布林變數 from_head 的值,以指定要使用 list_first_entry 還是 list_last_entry 來找到目標節點。

element_t *q_remove(struct list_head *head,
                    bool from_head,
                    char *sp,
                    size_t bufsize)
{
    if (!head || list_empty(head))
        return NULL;

    element_t *element;
    if (from_head)
        element = list_first_entry(head, element_t, list);
    else
        element = list_last_entry(head, element_t, list);
    list_del_init(&element->list);

    if (sp) {
        size_t len = strlen(element->value) + 1;
        memcpy(sp, element->value, len);
    }
    return element;
}

找到節點後,就可以用 list_del_init 函式把節點從鏈結串列中移除,並且把節點的 nextprev 指標初始化成指向自己,避免透過指標找到原有鏈結串列中的其他節點。

q_remove_headq_remove_tail 只是將節點從鏈結串列中移除,即切斷連接,並沒有釋放掉節點本身所佔用的記憶體空間。而釋放移除節點使用的記憶體這項操作,會在 qtest.c 中的 q_release_element(re); 完成。

按照 queue.h 的描述,當 buffer sp 不是空指標且有移除節點時,需要將被移除的節點的字串內容複製到 sp 指向的記憶體地址,所以使用 memcpy 函式來完成,其中複製長度為 strlen(element->value) + 1,其中 +1 是給字串的結束字元 \0。相似作法可以參考 harness.c 中的 test_strdup

這邊要注意的是,當 sp 不是空指標時,代表 sp 已經在 qtest.c 中透過 malloc 配置好記憶體,所以不能再使用 strdup 來複製字串,因為 strdup 會使用到 malloc。例如 sp = strdup(element->value);,如果使用 strdup 會出現以下錯誤。

ERROR: Failed to store removed value

修正放入 buffer sp 的字串的長度

原本以為 buffer sp 是存放所有節點的整個字串內容,但在 make test 發現過不了 trace-07-string。

+++ TESTING trace trace-07-string:
# Test of truncated strings
ERROR: copying of string in remove_head overflowed destination buffer.
ERROR: copying of string in remove_head overflowed destination buffer.
ERROR: copying of string in remove_head overflowed destination buffer.
ERROR: copying of string in remove_head overflowed destination buffer.
ERROR: copying of string in remove_head overflowed destination buffer.
Error limit exceeded.  Stopping command execution
---     trace-07-string 0/6

於是去查看測資發現有 option length 30 這種命令,這會改變 qtest.c 中的 string_length 全域變數,而 string_length 正是需要放入 buffer sp 的字元數量,所以我原本 q_remove 函式的實作是錯的,修正過後的程式碼如下:

    if (sp) {
-        size_t len = strlen(element->value) + 1;
-        memcpy(sp, element->value, len);
+        memcpy(sp, element->value, bufsize - 1);
+        sp[bufsize - 1] = '\0';
    }

傳入的 bufsizestring_length + 1+ 1 是給中止字元,所以 memcpy 複製的長度是 bufsize - 1sp[bufsize - 1] = '\0' 則是設定字串的中止字元。修改過後的程式碼即可通過測資。

q_size

Commit d1b2056

使用 list_for_each 走訪 linked list 上的所有節點,並使用變數 size 紀錄有多少個節點,然後回傳節點數。

q_delete_mid

Commit a0c6dfd

常見的做法是使用快慢指標找到中間節點,但我們的鏈結串列是雙向的,而非單向的,因此只需一行程式碼 (struct list_head *right = head->prev;) 就可以取得鏈結串列的最後一個節點。

作法是用兩個指標分別指向鏈結串列的第一個和最後一個節點,然後輪流遍歷鏈結串列,每次移動一個節點,直到兩個指標指向相同位址 (奇數個節點) 或者相鄰位址 (偶數個節點),此時就找到中間節點了。

這個方法總共只需要走訪 n 個節點 (n 是鏈結串列的節點數量),如果使用快慢指標策略則需要走訪 1.5n 個節點。

q_delete_dup

Commit 89b215d

建立鏈結串列 removed 來收集所有重複的節點。使用布林變數 has_duplicate 來紀錄是否遇到擁有重複字串的節點。在判斷字串是否相同時,使用 strcmp 即可,因為我們的字串內容都是只有一個單字。如果兩個字串擁有同樣內容,則 strcmp 會回傳 0,所以我們判斷相同的條件是 !strcmp(node->value, safe->value)

/* cppcheck-suppress unusedLabel */
list_for_each_entry_safe (node, safe, head, list) {
    if (&safe->list != head && !strcmp(node->value, safe->value)) {
        list_move_tail(&node->list, &removed);
        has_duplicate = true;
    } else if (has_duplicate) {
        list_move_tail(&node->list, &removed);
        has_duplicate = false;
    }
}

/* cppcheck-suppress unusedLabel */
list_for_each_entry_safe (node, safe, &removed, list)
    q_release_element(node);

當發現具有重複字元的節點時,第一個 if 條件會將第一個重複元素從原本的鏈結串列中移除,並將其加入到待刪除的鏈結串列 removed 中。接著,將 has_duplicate 設為 true,表示發現了一組包含重複字元的子鏈結串列。第二個 if 條件則確保這組具有重複字元的子鏈結串列的最後一個節點也會被移除。

在遍歷整個鏈結串列 head 後,可以確保鏈結串列 head 中不再包含具有重複字元的節點。隨後,使用 list_for_each_entry_safe 來釋放鏈結串列 removed 中節點所佔用的記憶體。

q_reverse

Commit 606cf98

使用 list_for_each_safe 函式遍歷鏈結串列中每個節點,然後把節點重新插入到鏈結串列中,即可達成反轉的效果。要達成重新插入需要使用 list_move 函式。

q_reverseK

Commit dd5e1f8

list_for_each_safe (node, safe, head) {
    count++;
    list_move(node, &sub_list);
    if (count == k) {
        list_splice_init(&sub_list, cur_head);
        cur_head = safe->prev;
        count = 0;
    }
}

函式定義了一個臨時的 list_head 結構 sub_list,用於收集每一組的 k 個節點,並透過一個計數器 count 追蹤已收集的節點數量,以及指標 cur_head 來紀錄當前處理的子鏈結串列的前一個節點,以方便反轉後更新鏈結。

在遍歷過程中,函數使用 list_for_each_safe 安全地逐一訪問鏈結串列中的每一個節點,並使用 list_move 函式將節點移到 sub_list 中,因為是插入到 sub_listhead,所以 sub_list 內的節點就相當於是反轉過後的節點,這個策略跟 q_reverse 是一致的。每當計數器達到 k 時,表示已收集到一組完整的 k 個節點 (已反轉順序),此時透過 list_splice_init 將這組節點拼接到 cur_head 之後的位址,並更新 cur_head 為當前這一組的結束節點(即 safe->prev),作為下一組子鏈結串列的前一個節點,同時重置計數器 count 為 0,即可準備處理下一組的 k 個節點。

對於鏈結串列尾端不足 k 個的剩餘節點(在 sub_list 中),函式會使用 q_reverse 將 sub_list 反轉,以回到原始鏈結串列的順序,因為題目要求不足 k 個的剩餘節點不能做反轉。之後在將反轉後的 sub_list 接回原始鏈結串列即可。

q_swap

Commit 311940e

結果與 K = 2q_reverseK 相同,呼叫 q_reverseK 即可。

q_ascend, q_descend

Commit 3d4c601

參考 slipet 的方法

q_ascend 是從鏈結串列的第一個節點開始訪問,往最後一個節點移動;q_descend 是從鏈結串列的最後一個節點開始訪問,往第一個節點移動。

兩種方法都是紀錄訪問過程中的最大值 (使用 strcmp 比較),若有發現比最大值還小的節點,則移除該節點,以維持鏈結串列的單調性。若有發現更大的,則更新最大值。

在提交程式碼時,原本 max 指標的型態是 char *,但這樣會不能通過 Cppcheck 的靜態分析。

Running static analysis...
queue.c:241:11: style: Variable 'max' can be declared as pointer to const [constVariablePointer]
    char *max = node->value;
          ^

Fail to pass static analysis.

而要忽略這種檢查有以下兩種方法:
方法 1

+ /* cppcheck-suppress constVariablePointer */
char *max = node->value;

方法 2

- char *max = node->value;
+ const char *max = node->value;

我最後選擇方法 1,因為指標指向的地址是會更改的,不是 const,所以採用方法 1 更合理。

q_sort

Commit 2f2c670

使用 top-down 方法來進行 merge sort,每次將鏈結串列從中間節點分割成兩個子鏈結串列,利用前後端指標(front-end pointer)遍歷來識別中間節點。q_sort 遞迴地對每一半進行排序,再使用 q_merge_two 輔助函式將排序後的兩個子鏈結串列合併成一個鏈結串列。

q_merge_two 會比較來自兩半的相鄰元素,使用 strcmp 根據 descend 參數(決定升序或降序)來構建排序後的串列,並將結果插入到 head 的尾端。

在提交程式碼時,Cppcheck 提示指標 l_elemr_elem 可以用來 const 來修飾。

Running static analysis...
queue.c:235:20: style: Variable 'l_elem' can be declared as pointer to const [constVariablePointer]
        element_t *l_elem = list_entry(l, element_t, list);
                   ^
queue.c:236:20: style: Variable 'r_elem' can be declared as pointer to const [constVariablePointer]
        element_t *r_elem = list_entry(r, element_t, list);
                   ^

Fail to pass static analysis.

const element_t * 型態的指標表示不能使用這個指標來改變其所指向變數的值,即不能用這個指標來更改節點的值。而在這個函式中,確實不會更改節點的值,只會存取節點的字串內容,所以加上 const 來修飾這個指標更為恰當。

-        element_t *l_elem = list_entry(l, element_t, list);
-        element_t *r_elem = list_entry(r, element_t, list);
+        const element_t *l_elem = list_entry(l, element_t, list);
+        const element_t *r_elem = list_entry(r, element_t, list);

q_merge

Commit b9d6913

q_merge 可以透過使用 q_merge_two 函式來完成,原理是把第一個佇列跟下一個佇列做合併,合併到暫時的 merged 佇列,然後再把 merged 佇列的結果移回第一個佇列。一直持續這樣的操作直到剩下一個佇列。

int queue_num = q_size(head);
while (queue_num > 1) {
    LIST_HEAD(merged);
    q_merge_two(&merged, current->q, next->q, descend);
    list_splice_tail_init(&merged, current->q);
    next = list_entry(next->chain.next, queue_contex_t, chain);
    queue_num--;
}

在原本的 q_merge_two 函式中,合併完後並沒有初始化 leftright 指向的鏈結串列,這樣的作法在 q_sort 是可行的,因為不會再透過 leftright 指標對鏈結串列做操作,但是在 q_merge 時,就會有這種操作,即以下程式碼:

list_splice_tail_init(&merged, current->q);

需要把合併後的結果再放回 current->q 第一個佇列,這裡的 current->q 一定要是空佇列,不然會出現無限迴圈,所以 q_merge_two 函式必須修改成合併完後再次初始化,q_merge_two 函式的差異如下:

    if (l != left)
-         list_splice_tail(left, head);
+         list_splice_tail_init(left, head);
     if (r != right)
-         list_splice_tail(right, head);
+         list_splice_tail_init(right, head);

運用 Valgrind 排除 qtest 實作的記憶體錯誤

到這邊為止 make test 的分數是 95/100

在 qtest 提供新的命令 shuffle

static void q_shuffle(struct list_head *head)
{
    if (!head || list_empty(head) || list_is_singular(head))
        return;

    int len = q_size(head);
    struct list_head *left;
    struct list_head *right = head;
    for (; len > 1; len--, right = right->prev) {
        int idx = rand() % len;
        if (idx == len - 1)
            continue;

        left = head;
        while (idx--)
            left = left->next;
        list_move(right->prev, left);
        list_move_tail(left->next->next, right);
    }
}

統計方法驗證 shuffle

  • 虛無假說
    H0
    :shuffle 後的各種可能結果發生的機率遵守 Uniform distribution。
  • 對立假說
    H1
    :shuffle 後的各種可能結果發生的機率至少有一個不相同。

1. 計算 chi-squared test statistic
X2

X2=i=1n(OiEi)2Ei

使用作業說明提供的測試用的 Python 腳本 shuffle_test.py 進行測試,得到以下統計數據與直方圖。

Expectation:  41666
Observation:  {'1234': 41425, '1243': 41658, '1324': 41691, '1342': 41614, '1423': 41792, '1432': 41607, '2134': 41396, '2143': 41771, '2314': 41734, '2341': 41592, '2413': 41732, '2431': 41848, '3124': 41714, '3142': 41449, '3214': 41958, '3241': 41661, '3412': 41948, '3421': 41756, '4123': 41851, '4132': 41362, '4213': 41551, '4231': 41631, '4312': 41199, '4321': 42060}
chi square sum:  22.777756444103108

test_shuffle

總共測試 1000000 次 shuffle,而可能組合有 4! = 24 種可能,所以每種可能的預期出現次數是

10000004!=41666。測試結果的卡方值
X2=17.3265492247876

2. 決定自由度

shuffle 4 個數字一共會有 4! 種結果,所以有 4! = 24 個樣本,自由度為 24 - 1 = 23。

3. 選擇顯著水準

設定顯著水準

α=0.05,代表在虛無假說(
H0
)成立的情況下,錯誤地拒絕
H0
(即判斷為不均勻但實際尚為均勻分佈,這叫 type I 錯誤)的機率控制在 0.05。

4. 結論

X2=17.3265492247876 和自由度為 23,從卡方分布表得知對應的
P
值介於 0.9 和 0.8 之間。

因為

P 值大於顯著水準 0.05,所以不拒絕
H0

若 P 值小於 0.05,則拒絕 H0 H_0 H0​。這意味著卡方值超過臨界值(右尾機率 < 0.05)時拒絕 H0 H_0 H0​。

image

但表我有 95% 的信心水準把握虛無假說(

H0)是可靠的,而剩下的 5% 是我們願意接受犯錯的風險。(拒絕
H0
時其實它是真的,這叫型 I 錯誤)。

Web 命令/網頁伺服器改善

Commit 2a9c179

在不修改原始程式碼情況下,在 qtest 命令直譯器中輸入 web 開啟網頁伺服器後,在另外一個終端機使用 curl 命令進行連線,例如以下命令:

$ curl http://localhost:9999/new
$ curl http://localhost:9999/ih/1

會在 qtest 命令直譯器中看到以下結果,沒有出現 favicon.ico 的問題。

cmd> web
listen on port 9999, fd is 3
cmd> 
l = []
cmd> 
l = [1]

但如果直接在網頁瀏覽器 (chrome) 輸入上方例子的那些網址,qtest 命令直譯器會出現不預期的結果。

cmd> web
listen on port 9999, fd is 3
cmd> 
l = []
cmd> 
Unknown command 'favicon.ico'
cmd> 
l = [1]
cmd> 
Unknown command 'favicon.ico'

佇列的操作是正確的,但多出現 Unknown command 'favicon.ico' 這種訊息,這是某些網頁瀏覽器會要求給予網頁圖案的需求。

接著觀察網頁瀏覽器的檢查工具的 Network 分頁,可以發現命令 new 有成功送出,而且 favicon.ico 的請求狀態 (200) 也是成功的,不是錯誤碼 404。

image
image
image

而在使用 send_response() 來回應後,會看到 Network 分頁有不一樣的結果。為了通過 pre-commit hook 的 fmtscan,

void send_response(int out_fd)
{
    char *buf = 
        "HTTP/1.1 200 OK\r\n%s%s%s%s%s%s"
        "Content-Type: text/html\r\n\r\n" 
        "<html><head>"
        "<style>body{font-family: cursive; font-size: 13px;}"
        "td {padding: 1.5px 6px;}"
        "</style><link rel=\"shortcut icon\" href=\"#\">"
        "</head><body><table>\n";
    web_send(out_fd, buf);
}

image
image
image

原本的 favicon.ico 不見了,反而多了一個新的請求 new,Request URL 變成了 http://localhost:9999/new,但是 Request Headers 卻沒有變。可以得知這個新的請求 new 是由原本的 favicon.ico 觸發的。再次使用網頁瀏覽器輸入網址會得到以下結果。

cmd> web
listen on port 9999, fd is 3
cmd> 
l = []
cmd> 
l = []
cmd> 
l = [1]
cmd> 
l = [1 1]

至於為何會再次觸發? 是因為 <link rel="shortcut icon" href="#"> 讓瀏覽器解析到這個 <link> 標籤後,會嘗試從 href="#" 指定的 URL 載入圖標。但因為 href="#" 實際上是指向當前頁面 (http://localhost:9999/new),所以網頁瀏覽器會再次發送一個 GET /new 請求,而不是 GET /favicon.ico,這也就是會多一個新的請求 new

要解決再次觸發問題,需要更改 href 的內容,有以下可能的方法:

  1. 提供正確的 favicon 位置,例如 href="images/c-icon.png",但這樣經過 parse_request 函式解析網址後,網頁就會導向 http://localhost:9999/images/c-icon.png,直譯器會出現 Unknown command 'images',這顯然不是我想要的。
  2. 導向給一個不做事的命令,例如 href="nothing",網頁就會導向 http://localhost:9999/nothing,並在 qtest.c 新增 do_nothing 函式並註冊。這樣可以保障伺服器正常運作,但會在 qtest 的直譯器中看到執行 nothing 這個命令,不是很美觀。
  3. 參考 BennyWang1007 的作法。把 href 改為空的 data url href="data:,",以防止送出重複的 request。
    image

經過方法 3 的修改,有很意外的發現:

  • 透過 curl 命令或者直接在 FireFox 執行上面的命令後,結果是正確的,直譯器也能再輸入任何命令。
  • 直接在 Chrome 的無痕視窗執行上面的命令後,結果是正確的,頁面也正確渲染,直譯器也能再輸入任何命令。
  • 直接在 Chrome (非無痕視窗) 執行上面的命令後,結果是正確的,但是直譯器沒有反應,不能再輸入任何字元,需要等待一段時間才能再輸入命令,而且等待過程中會再多執行一次命令 new

現在還沒有找出原因,但已經排除是 Chrome 的外掛插件問題。

傳送標準輸出的結果到網頁

參考 BennyWang1007 的 Commit 83ed391

console.c 中的全域變數 web_connfd 移動到 web.c 中。

在原本的 web_eventmux() 函式中,有關閉連線 web_connfd,但是在後面程式碼有使用到 report() 函式 ( 例如 q_show(3)),而在 report() 函式需要將原本輸出到標準輸出的結果也當作 HTTP response 傳回給 client,所以使用了 web_send() 函式,但此時連線已經在 web_eventmux() 中關閉了,這會造成錯誤。

所以需要把 web_eventmux() 中的 close(web_connfd) 移動到 report() 函式裡面。在傳送完標準輸出的結果後,再多傳送 "\0" 來表示中止,之後才關閉連線,然後重新設定 web_connfd

至於相關的 report_noreturn() 函式則不用做修改,因為使用該函式後,還會再呼叫 report() 函式,所以交給 report() 函式來關閉連線就好。

...
if (web_connfd) {
    ...
    web_send(web_connfd, buffer);
+    if (write(web_connfd, "\0", 1) == -1)
+        perror("write");
+    close(web_connfd);
+    web_connfd = 0;   
}
...

使用命令 show 無法顯示佇列資訊在網頁或 curl 命令

在直譯器中手動輸入 show

cmd> show
Current queue ID: 0
l = [1]

透過 curl 命令輸入 show

$ curl http://localhost:9999/show --output -
<html><head><style>body{font-family: monospace; font-size: 13px;}td {padding: 1.5px 6px;}</style><link rel="shortcut icon" href="data:,"></head><body><table>
Current queue ID: 0

可以發現使用 curl 命令來執行 show 命令,show 命令會呼叫兩次 report() 函式,但是每次呼叫 report() 函式都會檢查是否有連線,並關閉連線,所以在第一次呼叫 report() 函式,連線就會關閉,因此呼叫第二次 report() 函式時,就無法傳送 l = [1],因為連線已經關閉了。

解決辦法是新增關閉連線的條件,由 do_show() 裡面的 report(1, "Current queue ID: %d", current->id); 可以得知傳入 report() 函式的 fmt 是 "Current queue ID: %d",所以判斷條件就是 fmt 裡是否含有 Current queue ID,如果含有就提早回傳,不執行後面的關閉連線操作。

...
if (web_connfd) {
    ...
    web_send(web_connfd, buffer);
+     if (strstr(fmt, "Current queue ID")) {
+         if (write(web_connfd, "<br>\0", 5) == -1)
+             perror("write");
+         return;
+     }
    if (write(web_connfd, "\0", 1) == -1)
    ...
}
...

並且再多傳送 "<br>\0" 讓網頁伺服器能把渲染出換行,提昇可讀性,如下面的圖片 (FireFox)。
image

忽略 fmtscan 對 HTML 標籤屬性的檢查

當提交 commit 時,會出現以下提示:

Running fmtscan...
href
rel
7813 lines scanned (0.237M bytes)
53 printf style statements being processed
2 unique bad spellings found (2 non-unique)

[!] Check format strings for spelling

hrefrel 正是伺服器回應的 link 標籤的屬性,fmtscan 應該忽略對這些標籤屬性的檢查,所以我修改了 tools/fmtscan.c,新增了要忽略的標籤屬性。

+ /* HTML-related tags to ignore */
+ static char *ignore_html_tags[] = {
+     "href",
+     "rel",
+ };

+ static inline bool is_ignored_html_tag(const char *word)
+ {
+     for (size_t i = 0; i < SIZEOF_ARRAY(ignore_html_tags); i++) {
+         if (strcmp(word, ignore_html_tags[i]) == 0) {
+             return true;
+         }
+     }
+     return false;
+ }

static inline void add_bad_spelling(const char *word, const size_t len)
{
    if (find_word(word, printf_nodes, printf_node_heap))
        return;

+     if (is_ignored_html_tag(word))
+         return;
    ... 
}