网络连接与常用诊断
本节目标
用 loopback、操作系统分配的动态端口和运行时自带客户端完成本机网络诊断,并在没有受支持运行时时从固定访问日志得到同样可验证的离线结论。
开始前状态
你已完成 LINUX-05:config/env.dev-lab 精确包含两个允许键,notes/linux-notes.md 精确包含前五节。本节不会修改访问日志,也不会把实际 PID、端口、IP、响应头或其他瞬时信息写入 checkpoint。
必要原理
127.0.0.1 是 IPv4 loopback 地址,连接不会离开本机。把服务端口设为 0,是请求操作系统分配当前可用的动态端口,可避免猜测或占用固定端口。服务仍必须明确只绑定 loopback。
诊断顺序应从边界最小、结果最确定的检查开始:先验证本地状态,再启动只服务受控内容的本机临时服务。练习自动优先使用 python3,其次使用 node;两者都不存在时,只读既有六行访问日志,得到 requests=6、failures=2。
本页不依赖 curl,Python 分支使用标准库 urllib.request,Node 分支使用 node:http。公网 DNS 或 HTTP 观察只放在课后可选提示中,网络受限时可以完全跳过。
动手完成
网络块先验证完整 LINUX-05 路径、字节与权限,再创建唯一的 dev-lab/.tmp-linux-network。运行时服务只能读取其中新建的 index.txt;公共清理函数只处理保存的精确 PID、port、index.txt 和该空目录,所有退出与信号路径都会发送 TERM(若仍存活)并无条件 wait。
运行位置: 任意 Bash 工作目录,DEVLAB_DIR 指向完成 LINUX-05 的 dev-lab。预期输出: 三条分支都精确输出 network diagnosis: ok,结束后临时目录不存在。失败处理: 启动失败、端口等待超时、请求失败或主动中断时,先让 trap 清理本练习 PID 与命名临时路径;不要扩大清理范围。
linux_network_practice() (
set -euo pipefail
devlab_input="${DEVLAB_DIR:-}"
[[ -n "$devlab_input" ]] || { printf 'DEVLAB_DIR 必须是明确的练习目录。\n'; exit 1; }
[[ -d "$devlab_input" ]] || { printf 'DEVLAB_DIR 必须是真实目录。\n'; exit 1; }
devlab_logical="$(cd -- "$devlab_input" && pwd -L)" || exit 1
devlab_dir="$(cd -- "$devlab_input" && pwd -P)" || exit 1
[[ "$devlab_logical" == "$devlab_dir" && ! -L "$devlab_input" ]] || {
printf 'DEVLAB_DIR 不得经过符号链接。\n'
exit 1
}
[[ "$devlab_dir" != / ]] || { printf 'DEVLAB_DIR 必须是明确的练习目录。\n'; exit 1; }
if [[ -n "${HOME:-}" && -d "$HOME" ]]; then
home_dir="$(cd -- "$HOME" && pwd -P)" || exit 1
[[ "$devlab_dir" != "$home_dir" ]] || { printf 'DEVLAB_DIR 不得是用户主目录。\n'; exit 1; }
fi
DEVLAB_DIR="$devlab_dir"
for required_directory in config data notes output scripts; do
[[ -d "$DEVLAB_DIR/$required_directory" && ! -L "$DEVLAB_DIR/$required_directory" ]] || {
printf '前置目录必须是真实目录:%s\n' "$required_directory"
exit 1
}
done
expected_paths='README.md
config
config/bashrc.dev-lab
config/env.dev-lab
data
data/access.log
notes
notes/linux-notes.md
notes/shell-notes.md
output
output/summary.txt
scripts
scripts/permission-demo.sh
scripts/report.sh'
actual_paths="$(cd "$DEVLAB_DIR" && find . -mindepth 1 -print | sed 's#^\./##' | LC_ALL=C sort)"
[[ "$actual_paths" == "$expected_paths" ]] || { printf 'LINUX-05 路径集合不匹配。\n'; exit 1; }
require_exact() {
local relative_path="$1"
local expected_content="$2"
[[ -f "$DEVLAB_DIR/$relative_path" && ! -L "$DEVLAB_DIR/$relative_path" ]] \
&& cmp -s "$expected_content" "$DEVLAB_DIR/$relative_path" \
|| { printf '前置文件不匹配:%s\n' "$relative_path"; exit 1; }
}
has_any_execute_bit() {
local mode
mode="$(LC_ALL=C ls -ld "$1")"
mode="${mode%% *}"
[[ "$mode" == *[xstST]* ]]
}
require_exact 'README.md' <(printf '%s\n' \
'# dev-lab Shell checkpoint' \
'' \
'这是终端与 Shell 板块完成后的权威参考状态。它保留固定访问日志、确定性摘要、隔离 Bash 配置和只会写入明确输出文件的报告脚本。' \
'' \
'请在此目录的父目录运行 `check-shell.sh`;checker 只读取目标,不会 source 配置或执行报告脚本。')
require_exact 'config/bashrc.dev-lab' <(printf '%s\n' \
"export DEVLAB_MODE='practice'" \
'' \
'devlab-root() {' \
' printf '\''%s\n'\'' "${DEVLAB_DIR:?set DEVLAB_DIR before using devlab-root}"' \
'}' \
'' \
'devlab-summary() {' \
' bash "${DEVLAB_DIR:?set DEVLAB_DIR first}/scripts/report.sh" "$DEVLAB_DIR"' \
'}')
require_exact 'config/env.dev-lab' <(printf '%s\n' 'DEVLAB_PROFILE=practice' 'DEVLAB_REGION=local')
require_exact 'data/access.log' <(printf '%s\n' \
'2026-08-12T09:00:00Z GET / 200' \
'2026-08-12T09:00:01Z GET /docs/devenv/welcome 200' \
'2026-08-12T09:00:02Z GET /docs/devenv/module-route 200' \
'2026-08-12T09:00:03Z GET /missing 404' \
'2026-08-12T09:00:04Z POST /practice 200' \
'2026-08-12T09:00:05Z GET /old-link 404')
require_exact 'notes/shell-notes.md' <(printf '%s\n' \
'# Shell 练习笔记' \
'' \
'历史记录可能包含参数、路径或令牌;先检查再分享或清理。Tab 补全帮助确认命令和文件名,但不代替阅读实际路径。' \
'' \
'本 checkpoint 的 Bash 配置仅用于 `DEVLAB_DIR` 指定的隔离练习目录。')
require_exact 'output/summary.txt' <(printf '%s\n' 'total requests: 6' 'successful requests: 4' 'not found: 2')
require_exact 'scripts/report.sh' <(printf '%s\n' \
'#!/usr/bin/env bash' \
'set -euo pipefail' \
'' \
'[[ "$#" -eq 1 ]] || { printf '\''usage: report.sh DEV_LAB_DIR\n'\'' >&2; exit 64; }' \
'devlab_dir="${1:?usage: report.sh DEV_LAB_DIR}"' \
'input="$devlab_dir/data/access.log"' \
'output="$devlab_dir/output/summary.txt"' \
'[[ -f "$input" ]] || { printf '\''missing input: %s\n'\'' "$input" >&2; exit 1; }' \
'' \
'total="$(wc -l < "$input" | tr -d '\'' '\'')"' \
'successful="$(grep -c '\'' 200$'\'' "$input" || true)"' \
'not_found="$(grep -c '\'' 404$'\'' "$input" || true)"' \
'' \
'{' \
' printf '\''total requests: %s\n'\'' "$total"' \
' printf '\''successful requests: %s\n'\'' "$successful"' \
' printf '\''not found: %s\n'\'' "$not_found"' \
'} > "$output"')
require_exact 'scripts/permission-demo.sh' <(printf '%s\n' \
'#!/usr/bin/env bash' \
'set -euo pipefail' \
'' \
'printf '\''permission demo: ok\n'\''')
require_exact 'notes/linux-notes.md' <(printf '%s\n' \
'# Linux 基础练习笔记' \
'' \
'## 文件系统' \
'- 系统目录只观察;所有练习写入明确的 dev-lab。' \
'- Linux 与 WSL 的用户目录通常位于 /home,macOS 用户目录位于 /Users。' \
'' \
'## 用户、用户组与权限' \
'- 先确认目标是非符号链接普通文件,再只增加用户执行位。' \
'' \
'## 进程、任务与信号' \
'- 只终止本练习刚启动并保存 PID 的进程,随后 wait 完成回收。' \
'' \
'## 软件包与命令' \
'- 先确认命令来源和包管理器;模拟或查看信息不等于真实安装。' \
'' \
'## 环境变量与配置' \
'- 子进程只继承已导出的变量;项目配置只解析允许的键和值。')
for relative_path in README.md config/bashrc.dev-lab config/env.dev-lab data/access.log notes/shell-notes.md notes/linux-notes.md output/summary.txt; do
! has_any_execute_bit "$DEVLAB_DIR/$relative_path" || { printf '前置文件不得可执行:%s\n' "$relative_path"; exit 1; }
done
[[ -x "$DEVLAB_DIR/scripts/report.sh" && -x "$DEVLAB_DIR/scripts/permission-demo.sh" ]] || exit 1
BASH_ENV= ENV= bash --noprofile --norc -n "$DEVLAB_DIR/scripts/report.sh"
BASH_ENV= ENV= bash --noprofile --norc -n "$DEVLAB_DIR/scripts/permission-demo.sh"
BASH_ENV= ENV= bash --noprofile --norc -n "$DEVLAB_DIR/config/bashrc.dev-lab"
server_pid=''
network_tmp="$DEVLAB_DIR/.tmp-linux-network"
network_tmp_owned=0
network_tmp_identity=''
network_index="$network_tmp/index.txt"
network_index_identity=''
network_index_owned=0
network_port="$network_tmp/port"
network_port_identity=''
network_port_owned=0
network_file_identity() {
local target="$1"
local identity
if identity="$(LC_ALL=C stat -L -c '%d:%i' "$target" 2>/dev/null)"; then :; else
identity="$(LC_ALL=C stat -L -f '%d:%i' "$target")" || return 1
fi
printf '%s' "$identity"
}
network_fd_matches_identity() {
local fd_identity
fd_identity="$(network_file_identity "$1")" || return 1
[[ "$fd_identity" == "$2" || "$(/usr/bin/uname -s 2>/dev/null || /bin/uname -s)" == Darwin \
&& "${fd_identity##*:}" == "${2##*:}" ]]
}
network_context_matches() {
[[ "$(network_file_identity . 2>/dev/null)" == "$network_tmp_identity" \
&& "$(network_file_identity .. 2>/dev/null)" == "$network_root_identity" ]]
}
remove_network_file_if_owned() {
local owned_path="$1"
local owned_identity="$2"
local owned_name="${owned_path##*/}"
[[ -n "$owned_identity" ]] || return 0
if [[ "$owned_name" == port ]]; then
network_port_cleanup_boundary=1
else
network_index_cleanup_boundary=1
fi
network_context_matches || return 1
if [[ -f "./$owned_name" && ! -L "./$owned_name" \
&& "$(network_file_identity "./$owned_name" 2>/dev/null)" == "$owned_identity" ]]; then
/bin/rm -f -- "./$owned_name"
elif [[ -e "./$owned_name" || -L "./$owned_name" ]]; then
return 1
fi
}
cleanup_network() {
local cleanup_status=0
if test -n "$server_pid" && kill -0 "$server_pid" 2>/dev/null; then
kill -TERM "$server_pid" 2>/dev/null || true
fi
if test -n "$server_pid"; then
wait "$server_pid" 2>/dev/null || true
server_pid=''
fi
exec 8<&- 2>/dev/null || true
exec 7>&- 2>/dev/null || true
if [[ "$network_tmp_owned" -eq 1 ]]; then
network_cleanup_boundary=1
if ! network_context_matches; then
if [[ "$(network_file_identity . 2>/dev/null)" == "$network_root_identity" \
&& -d ./.tmp-linux-network && ! -L ./.tmp-linux-network \
&& "$(network_file_identity ./.tmp-linux-network 2>/dev/null)" == "$network_tmp_identity" ]]; then
cd -P -- ./.tmp-linux-network || cleanup_status=1
else
cleanup_status=1
fi
fi
if network_context_matches; then
remove_network_file_if_owned "$network_port" "$network_port_identity" || cleanup_status=1
remove_network_file_if_owned "$network_index" "$network_index_identity" || cleanup_status=1
if [[ "$cleanup_status" -eq 0 ]] && (
cd -P -- .. || exit 1
[[ "$(network_file_identity .)" == "$network_root_identity" \
&& -d ./.tmp-linux-network && ! -L ./.tmp-linux-network \
&& "$(network_file_identity ./.tmp-linux-network)" == "$network_tmp_identity" ]] || exit 1
/bin/rmdir -- ./.tmp-linux-network
); then
network_tmp_owned=0
else
cleanup_status=1
fi
else
cleanup_status=1
fi
fi
return "$cleanup_status"
}
trap cleanup_network EXIT
trap 'cleanup_network; exit 129' HUP
trap 'cleanup_network; exit 130' INT
trap 'cleanup_network; exit 143' TERM
network_root_identity="$(network_file_identity "$DEVLAB_DIR")" || exit 1
cd -P -- "$DEVLAB_DIR" || exit 1
[[ "$(network_file_identity .)" == "$network_root_identity" ]] || exit 1
[[ ! -e ./.tmp-linux-network && ! -L ./.tmp-linux-network ]] \
|| { printf '网络临时路径已存在,停止并检查。\n' >&2; exit 1; }
/bin/mkdir -- ./.tmp-linux-network || exit 1
network_tmp_identity="$(network_file_identity ./.tmp-linux-network)" || exit 1
network_tmp_owned=1
network_tmp_post_create_boundary=1
[[ -d ./.tmp-linux-network && ! -L ./.tmp-linux-network \
&& "$(network_file_identity .)" == "$network_root_identity" ]] || exit 1
cd -P -- "$network_tmp" || exit 1
network_context_matches || exit 1
set -o noclobber
exec 6> ./index.txt || { set +o noclobber; exit 1; }
set +o noclobber
network_index_identity="$(network_file_identity ./index.txt)" || { exec 6>&-; exit 1; }
network_index_owned=1
network_index_post_create_boundary=1
network_fd_matches_identity /dev/fd/6 "$network_index_identity" || { exec 6>&-; exit 1; }
printf 'dev-lab loopback ok\n' >&6
network_index_post_write_boundary=1
chmod u=rw,go=r /dev/fd/6
network_index_post_mode_boundary=1
network_fd_matches_identity /dev/fd/6 "$network_index_identity" || { exec 6>&-; exit 1; }
[[ -f ./index.txt && ! -L ./index.txt \
&& "$(network_file_identity ./index.txt)" == "$network_index_identity" ]] \
&& network_context_matches || { exec 6>&-; exit 1; }
exec 6>&-
set -o noclobber
exec 7> ./port || { set +o noclobber; exit 1; }
set +o noclobber
network_port_identity="$(network_file_identity ./port)" || { exec 7>&-; exit 1; }
network_port_owned=1
network_port_post_create_boundary=1
network_fd_matches_identity /dev/fd/7 "$network_port_identity" || { exec 7>&-; exit 1; }
chmod u=rw,go= /dev/fd/7
network_port_post_mode_boundary=1
[[ -f ./port && ! -L ./port \
&& "$(network_file_identity ./port)" == "$network_port_identity" ]] \
&& network_context_matches || { exec 7>&-; exit 1; }
exec 8< ./port || { exec 7>&-; exit 1; }
network_fd_matches_identity /dev/fd/8 "$network_port_identity" || { exec 8<&-; exec 7>&-; exit 1; }
export DEVLAB_PORT_FD=7
if command -v python3 >/dev/null 2>&1; then
python3 -c "
import http.server
import os
import sys
class Handler(http.server.BaseHTTPRequestHandler):
def do_GET(self):
if self.path != '/index.txt':
self.send_error(404)
return
body = b'dev-lab loopback ok\n'
self.send_response(200)
self.send_header('Content-Type', 'text/plain')
self.send_header('Content-Length', str(len(body)))
self.end_headers()
self.wfile.write(body)
def log_message(self, format, *args):
pass
server = http.server.ThreadingHTTPServer(('127.0.0.1', 0), Handler)
os.write(int(os.environ['DEVLAB_PORT_FD']), (str(server.server_address[1]) + '\\n').encode('ascii'))
server.serve_forever()
" &
server_pid=$!
runtime=python3
elif command -v node >/dev/null 2>&1; then
node -e "
const fs = require('node:fs');
const http = require('node:http');
const body = Buffer.from('dev-lab loopback ok\\n');
const server = http.createServer((request, response) => {
if (request.method !== 'GET' || request.url !== '/index.txt') {
response.writeHead(404);
response.end();
return;
}
response.writeHead(200, {'Content-Type': 'text/plain'});
response.end(body);
});
server.listen(0, '127.0.0.1', () => {
fs.writeSync(Number(process.env.DEVLAB_PORT_FD), String(server.address().port) + '\\n');
});
" &
server_pid=$!
runtime=node
else
runtime=offline
fi
if [[ "$runtime" != offline ]]; then
attempts=0
while [[ ! -s /dev/fd/7 ]]; do
[[ -f ./port && ! -L ./port \
&& "$(network_file_identity ./port)" == "$network_port_identity" ]] \
&& network_context_matches || exit 1
kill -0 "$server_pid" 2>/dev/null || { printf '本机服务启动失败。\n' >&2; exit 1; }
attempts=$((attempts + 1))
[[ "$attempts" -lt 100 ]] || { printf '等待动态端口超时。\n' >&2; exit 1; }
sleep 0.01
done
port_file=./port
[[ -f "$port_file" && ! -L "$port_file" ]] || { printf '动态端口文件必须是普通文件。\n' >&2; exit 1; }
[[ "$(network_file_identity "$port_file")" == "$network_port_identity" \
&& "$(network_file_identity ./index.txt)" == "$network_index_identity" ]] \
&& network_context_matches || exit 1
network_fd_matches_identity /dev/fd/8 "$network_port_identity" || exit 1
IFS= read -r port <&8 || { printf '动态端口文件必须以换行结束。\n' >&2; exit 1; }
extra_port_content=''
if IFS= read -r extra_port_content <&8 || [[ -n "$extra_port_content" ]]; then
printf '动态端口文件只能有一行。\n' >&2
exit 1
fi
[[ "$port" =~ ^[0-9]+$ && "$port" -ge 1 && "$port" -le 65535 ]] || exit 1
if [[ "$runtime" == python3 ]]; then
python3 -c "
import sys
import urllib.request
with urllib.request.urlopen('http://127.0.0.1:' + sys.argv[1] + '/index.txt', timeout=2) as response:
body = response.read()
if body != b'dev-lab loopback ok\\n':
raise SystemExit(1)
" "$port"
else
node -e "
const http = require('node:http');
const port = Number(process.argv[1]);
const request = http.get({hostname: '127.0.0.1', port, path: '/index.txt'}, response => {
const chunks = [];
response.on('data', chunk => chunks.push(chunk));
response.on('end', () => {
const body = Buffer.concat(chunks).toString('utf8');
process.exit(response.statusCode === 200 && body === 'dev-lab loopback ok\\n' ? 0 : 1);
});
});
request.setTimeout(2000, () => request.destroy());
request.on('error', () => process.exit(1));
" "$port"
fi
else
requests=0
failures=0
while IFS=' ' read -r timestamp method route status extra; do
[[ -n "$timestamp" && -n "$method" && -n "$route" && -n "$status" && -z "${extra:-}" ]] || exit 1
[[ "$status" == 200 || "$status" == 404 ]] || exit 1
requests=$((requests + 1))
if [[ "$status" == 404 ]]; then failures=$((failures + 1)); fi
done < "$DEVLAB_DIR/data/access.log"
expected_requests=6
expected_failures=2
[[ "$requests" -eq "$expected_requests" && "$failures" -eq "$expected_failures" ]] || exit 1
fi
cleanup_network
[[ ! -e "$network_tmp" && ! -L "$network_tmp" ]] \
|| { printf '网络临时目录清理失败。\n' >&2; exit 1; }
printf 'network diagnosis: ok\n'
)
linux_network_practice
practice_status="$?"
unset -f linux_network_practice
[[ "$practice_status" -eq 0 ]] || exit "$practice_status"
练习成功后只推进规范笔记。状态迁移再次验证完整 LINUX-05;它不保存刚才的运行时分支、端口、PID、IP 或响应头,也不修改 data/access.log。
运行位置: 任意 Bash 工作目录,DEVLAB_DIR 指向完成 LINUX-05 且网络临时目录已清理的 dev-lab。预期输出: 无输出,Linux 笔记只原子新增“网络诊断”一节。失败处理: 任一前置检查失败都保持 snapshot 不变;不要跳过临时目录清理后强行迁移。
advance_linux_network_notes() (
set -euo pipefail
devlab_input="${DEVLAB_DIR:-}"
[[ -n "$devlab_input" ]] || { printf 'DEVLAB_DIR 必须是明确的练习目录。\n'; exit 1; }
[[ -d "$devlab_input" ]] || { printf 'DEVLAB_DIR 必须是真实目录。\n'; exit 1; }
devlab_logical="$(cd -- "$devlab_input" && pwd -L)" || exit 1
devlab_dir="$(cd -- "$devlab_input" && pwd -P)" || exit 1
[[ "$devlab_logical" == "$devlab_dir" && ! -L "$devlab_input" ]] || {
printf 'DEVLAB_DIR 不得经过符号链接。\n'
exit 1
}
[[ "$devlab_dir" != / ]] || { printf 'DEVLAB_DIR 必须是明确的练习目录。\n'; exit 1; }
if [[ -n "${HOME:-}" && -d "$HOME" ]]; then
home_dir="$(cd -- "$HOME" && pwd -P)" || exit 1
[[ "$devlab_dir" != "$home_dir" ]] || { printf 'DEVLAB_DIR 不得是用户主目录。\n'; exit 1; }
fi
for required_directory in config data notes output scripts; do
[[ -d "$devlab_dir/$required_directory" && ! -L "$devlab_dir/$required_directory" ]] || exit 1
done
expected_paths='README.md
config
config/bashrc.dev-lab
config/env.dev-lab
data
data/access.log
notes
notes/linux-notes.md
notes/shell-notes.md
output
output/summary.txt
scripts
scripts/permission-demo.sh
scripts/report.sh'
actual_paths="$(cd "$devlab_dir" && find . -mindepth 1 -print | sed 's#^\./##' | LC_ALL=C sort)"
[[ "$actual_paths" == "$expected_paths" ]] || { printf 'LINUX-05 路径集合不匹配。\n'; exit 1; }
require_exact() {
local relative_path="$1"
local expected_content="$2"
[[ -f "$devlab_dir/$relative_path" && ! -L "$devlab_dir/$relative_path" ]] \
&& cmp -s "$expected_content" "$devlab_dir/$relative_path" || exit 1
}
has_any_execute_bit() {
local mode
mode="$(LC_ALL=C ls -ld "$1")"
mode="${mode%% *}"
[[ "$mode" == *[xstST]* ]]
}
require_exact 'README.md' <(printf '%s\n' \
'# dev-lab Shell checkpoint' \
'' \
'这是终端与 Shell 板块完成后的权威参考状态。它保留固定访问日志、确定性摘要、隔离 Bash 配置和只会写入明确输出文件的报告脚本。' \
'' \
'请在此目录的父目录运行 `check-shell.sh`;checker 只读取目标,不会 source 配置或执行报告脚本。')
require_exact 'config/bashrc.dev-lab' <(printf '%s\n' \
"export DEVLAB_MODE='practice'" \
'' \
'devlab-root() {' \
' printf '\''%s\n'\'' "${DEVLAB_DIR:?set DEVLAB_DIR before using devlab-root}"' \
'}' \
'' \
'devlab-summary() {' \
' bash "${DEVLAB_DIR:?set DEVLAB_DIR first}/scripts/report.sh" "$DEVLAB_DIR"' \
'}')
require_exact 'config/env.dev-lab' <(printf '%s\n' 'DEVLAB_PROFILE=practice' 'DEVLAB_REGION=local')
require_exact 'data/access.log' <(printf '%s\n' \
'2026-08-12T09:00:00Z GET / 200' \
'2026-08-12T09:00:01Z GET /docs/devenv/welcome 200' \
'2026-08-12T09:00:02Z GET /docs/devenv/module-route 200' \
'2026-08-12T09:00:03Z GET /missing 404' \
'2026-08-12T09:00:04Z POST /practice 200' \
'2026-08-12T09:00:05Z GET /old-link 404')
require_exact 'notes/shell-notes.md' <(printf '%s\n' \
'# Shell 练习笔记' \
'' \
'历史记录可能包含参数、路径或令牌;先检查再分享或清理。Tab 补全帮助确认命令和文件名,但不代替阅读实际路径。' \
'' \
'本 checkpoint 的 Bash 配置仅用于 `DEVLAB_DIR` 指定的隔离练习目录。')
require_exact 'output/summary.txt' <(printf '%s\n' 'total requests: 6' 'successful requests: 4' 'not found: 2')
require_exact 'scripts/report.sh' <(printf '%s\n' \
'#!/usr/bin/env bash' \
'set -euo pipefail' \
'' \
'[[ "$#" -eq 1 ]] || { printf '\''usage: report.sh DEV_LAB_DIR\n'\'' >&2; exit 64; }' \
'devlab_dir="${1:?usage: report.sh DEV_LAB_DIR}"' \
'input="$devlab_dir/data/access.log"' \
'output="$devlab_dir/output/summary.txt"' \
'[[ -f "$input" ]] || { printf '\''missing input: %s\n'\'' "$input" >&2; exit 1; }' \
'' \
'total="$(wc -l < "$input" | tr -d '\'' '\'')"' \
'successful="$(grep -c '\'' 200$'\'' "$input" || true)"' \
'not_found="$(grep -c '\'' 404$'\'' "$input" || true)"' \
'' \
'{' \
' printf '\''total requests: %s\n'\'' "$total"' \
' printf '\''successful requests: %s\n'\'' "$successful"' \
' printf '\''not found: %s\n'\'' "$not_found"' \
'} > "$output"')
require_exact 'scripts/permission-demo.sh' <(printf '%s\n' \
'#!/usr/bin/env bash' \
'set -euo pipefail' \
'' \
'printf '\''permission demo: ok\n'\''')
require_exact 'notes/linux-notes.md' <(printf '%s\n' \
'# Linux 基础练习笔记' \
'' \
'## 文件系统' \
'- 系统目录只观察;所有练习写入明确的 dev-lab。' \
'- Linux 与 WSL 的用户目录通常位于 /home,macOS 用户目录位于 /Users。' \
'' \
'## 用户、用户组与权限' \
'- 先确认目标是非符号链接普通文件,再只增加用户执行位。' \
'' \
'## 进程、任务与信号' \
'- 只终止本练习刚启动并保存 PID 的进程,随后 wait 完成回收。' \
'' \
'## 软件包与命令' \
'- 先确认命令来源和包管理器;模拟或查看信息不等于真实安装。' \
'' \
'## 环境变量与配置' \
'- 子进程只继承已导出的变量;项目配置只解析允许的键和值。')
for relative_path in README.md config/bashrc.dev-lab config/env.dev-lab data/access.log notes/shell-notes.md notes/linux-notes.md output/summary.txt; do
! has_any_execute_bit "$devlab_dir/$relative_path" || exit 1
done
[[ -x "$devlab_dir/scripts/report.sh" && -x "$devlab_dir/scripts/permission-demo.sh" ]] || exit 1
BASH_ENV= ENV= bash --noprofile --norc -n "$devlab_dir/scripts/report.sh"
BASH_ENV= ENV= bash --noprofile --norc -n "$devlab_dir/scripts/permission-demo.sh"
BASH_ENV= ENV= bash --noprofile --norc -n "$devlab_dir/config/bashrc.dev-lab"
notes_target="$devlab_dir/notes/linux-notes.md"
notes_draft="$devlab_dir/notes/.linux-notes.md.next"
notes_dir="$devlab_dir/notes"
file_identity() {
local identity
if identity="$(LC_ALL=C stat -L -c '%d:%i' "$1" 2>/dev/null)"; then :; else
identity="$(LC_ALL=C stat -L -f '%d:%i' "$1")" || return 1
fi
printf '%s' "$identity"
}
fd_matches_identity() {
local fd_identity
fd_identity="$(file_identity "$1")" || return 1
[[ "$fd_identity" == "$2" || "$(/usr/bin/uname -s 2>/dev/null || /bin/uname -s)" == Darwin \
&& "${fd_identity##*:}" == "${2##*:}" ]]
}
remove_if_owned() {
local owned_path="$1" owned_identity="$2" owned_name="${1##*/}"
[[ -n "$owned_identity" ]] || return 0
(
cd -P -- "$notes_dir" || exit 1
cleanup_owned_boundary="$owned_name"
[[ "$(file_identity .)" == "$notes_dir_identity" \
&& "$(file_identity ..)" == "$devlab_identity" ]] || exit 1
[[ ! -e "./$owned_name" && ! -L "./$owned_name" ]] && exit 0
[[ -f "./$owned_name" && ! -L "./$owned_name" \
&& "$(file_identity "./$owned_name" 2>/dev/null)" == "$owned_identity" ]] || exit 1
/bin/rm -f -- "./$owned_name"
)
}
create_owned_file() {
local owned_path="$1" parent_path="$2" parent_identity="$3" mode="$4" identity_variable="$5"
local owned_name="${owned_path##*/}" caller_directory="$PWD" created_identity=''
cd -P -- "$parent_path" || return 1
[[ "$(file_identity .)" == "$parent_identity" \
&& "$(file_identity ..)" == "$devlab_identity" ]] || return 1
set -o noclobber
exec 6> "./$owned_name" || { set +o noclobber; return 1; }
set +o noclobber
created_identity="$(file_identity "./$owned_name")" || { exec 6>&-; return 1; }
printf -v "$identity_variable" '%s' "$created_identity"
owned_post_create_boundary="$owned_name"
fd_matches_identity /dev/fd/6 "$created_identity" || { exec 6>&-; return 1; }
/bin/cat >&6 || { exec 6>&-; return 1; }
owned_post_write_boundary="$owned_name"
/bin/chmod "$mode" /dev/fd/6 || { exec 6>&-; return 1; }
owned_post_mode_boundary="$owned_name"
fd_matches_identity /dev/fd/6 "$created_identity" || { exec 6>&-; return 1; }
[[ -f "./$owned_name" && ! -L "./$owned_name" \
&& "$(file_identity "./$owned_name")" == "$created_identity" \
&& "$(file_identity .)" == "$parent_identity" \
&& "$(file_identity ..)" == "$devlab_identity" ]] || { exec 6>&-; return 1; }
exec 6>&-
cd -P -- "$caller_directory" || return 1
}
publish_owned_file() {
local owned_path="$1" owned_identity="$2" final_path="$3" parent_identity="$4" expected_final_identity="$5"
local parent_path="${owned_path%/*}" owned_name="${owned_path##*/}" final_name="${final_path##*/}"
(
cd -P -- "$parent_path" || exit 1
publication_boundary="$owned_name->$final_name"
[[ "$(file_identity .)" == "$parent_identity" \
&& "$(file_identity ..)" == "$devlab_identity" ]] || exit 1
[[ -f "./$owned_name" && ! -L "./$owned_name" \
&& "$(file_identity "./$owned_name")" == "$owned_identity" ]] || exit 1
[[ -f "./$final_name" && ! -L "./$final_name" \
&& "$(file_identity "./$final_name")" == "$expected_final_identity" ]] || exit 1
publication_commit_boundary="$owned_name->$final_name"
[[ -f "./$owned_name" && ! -L "./$owned_name" \
&& "$(file_identity "./$owned_name")" == "$owned_identity" \
&& -f "./$final_name" && ! -L "./$final_name" \
&& "$(file_identity "./$final_name")" == "$expected_final_identity" ]] || exit 1
if [[ "$(/usr/bin/uname -s 2>/dev/null || /bin/uname -s)" == Darwin ]]; then
/bin/mv -f -h -- "./$owned_name" "./$final_name" || exit 1
else
/bin/mv -f -T -- "./$owned_name" "./$final_name" || exit 1
fi
[[ ! -e "./$owned_name" && ! -L "./$owned_name" \
&& -f "./$final_name" && ! -L "./$final_name" \
&& "$(file_identity "./$final_name")" == "$owned_identity" \
&& "$(file_identity .)" == "$parent_identity" \
&& "$(file_identity ..)" == "$devlab_identity" ]]
)
}
devlab_identity="$(file_identity "$devlab_dir")" || exit 1
notes_dir_identity="$(file_identity "$notes_dir")" || exit 1
notes_target_identity="$(file_identity "$notes_target")" || exit 1
[[ ! -e "$notes_draft" && ! -L "$notes_draft" ]] || { printf '笔记草稿路径已存在,停止并检查。\n'; exit 1; }
notes_draft_identity=''
cleanup_linux_network_draft() {
local status="$?"
trap - EXIT
remove_if_owned "$notes_draft" "$notes_draft_identity" || exit 1
exit "$status"
}
trap cleanup_linux_network_draft EXIT
create_owned_file "$notes_draft" "$notes_dir" "$notes_dir_identity" u=rw,go=r notes_draft_identity <<'EXPECTED'
# Linux 基础练习笔记
## 文件系统
- 系统目录只观察;所有练习写入明确的 dev-lab。
- Linux 与 WSL 的用户目录通常位于 /home,macOS 用户目录位于 /Users。
## 用户、用户组与权限
- 先确认目标是非符号链接普通文件,再只增加用户执行位。
## 进程、任务与信号
- 只终止本练习刚启动并保存 PID 的进程,随后 wait 完成回收。
## 软件包与命令
- 先确认命令来源和包管理器;模拟或查看信息不等于真实安装。
## 环境变量与配置
- 子进程只继承已导出的变量;项目配置只解析允许的键和值。
## 网络诊断
- 必做练习只使用 loopback 或固定离线日志,不依赖公网。
EXPECTED
cmp -s "$notes_draft" <(printf '%s\n' \
'# Linux 基础练习笔记' \
'' \
'## 文件系统' \
'- 系统目录只观察;所有练习写入明确的 dev-lab。' \
'- Linux 与 WSL 的用户目录通常位于 /home,macOS 用户目录位于 /Users。' \
'' \
'## 用户、用户组与权限' \
'- 先确认目标是非符号链接普通文件,再只增加用户执行位。' \
'' \
'## 进程、任务与信号' \
'- 只终止本练习刚启动并保存 PID 的进程,随后 wait 完成回收。' \
'' \
'## 软件包与命令' \
'- 先确认命令来源和包管理器;模拟或查看信息不等于真实安装。' \
'' \
'## 环境变量与配置' \
'- 子进程只继承已导出的变量;项目配置只解析允许的键和值。' \
'' \
'## 网络诊断' \
'- 必做练习只使用 loopback 或固定离线日志,不依赖公网。') || exit 1
! has_any_execute_bit "$notes_draft" || exit 1
[[ "$(file_identity "$notes_dir")" == "$notes_dir_identity" \
&& "$(file_identity "$notes_draft")" == "$notes_draft_identity" ]] || exit 1
publish_owned_file "$notes_draft" "$notes_draft_identity" "$notes_target" "$notes_dir_identity" "$notes_target_identity"
[[ "$(file_identity "$notes_target")" == "$notes_draft_identity" ]] || exit 1
trap - EXIT
)
advance_linux_network_notes
transition_status="$?"
unset -f advance_linux_network_notes
[[ "$transition_status" -eq 0 ]] || exit "$transition_status"
观察结果
有 Python 时优先选择 Python,即使 Node 同时存在;没有 Python 时才选择 Node;两个运行时都不存在时,固定日志仍给出 6 个请求与 2 个失败。三条路径都只输出同一成功行,且 checkpoint 不保留临时网络状态。
常见问题
- 为什么不用固定端口? 动态端口由操作系统分配,减少与其他本机服务冲突的机会。
- 为什么不用 curl? 运行时自带客户端足以验证练习服务,也让离线回退不依赖额外命令。
- 可以测试公网吗? 可以作为非必做观察,但它受 DNS、代理、防火墙和服务状态影响,不能作为本节完成条件。
完成检查
- 成功 stdout 只有
network diagnosis: ok。 -
.tmp-linux-network、服务子进程与监听端口都已清理。 -
data/access.log字节不变,Linux 笔记精确包含前六节。
下一步
下一节将使用另一份固定日志建立基础排错顺序,并生成 Linux checkpoint 的最终健康报告。