<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Qian_Xiao</title>
    <description>The latest articles on DEV Community by Qian_Xiao (@pbxqdown).</description>
    <link>https://dev.to/pbxqdown</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4121297%2F1ec7112f-7c6b-4e65-a6f4-eface667d4f5.jpg</url>
      <title>DEV Community: Qian_Xiao</title>
      <link>https://dev.to/pbxqdown</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/pbxqdown"/>
    <language>en</language>
    <item>
      <title>Why Disk Space Can Stay Full After a Huge Log File Is Deleted</title>
      <dc:creator>Qian_Xiao</dc:creator>
      <pubDate>Mon, 26 Oct 2026 20:48:41 +0000</pubDate>
      <link>https://dev.to/pbxqdown/why-disk-space-can-stay-full-after-a-huge-log-file-is-deleted-305i</link>
      <guid>https://dev.to/pbxqdown/why-disk-space-can-stay-full-after-a-huge-log-file-is-deleted-305i</guid>
      <description>&lt;p&gt;A server runs out of disk space. The largest log file is found, deleted, and confirmed absent from its directory. Yet &lt;code&gt;df&lt;/code&gt; still reports the filesystem as full.&lt;/p&gt;

&lt;p&gt;The common misconception is simple: deleting a file immediately releases all of its disk space.&lt;/p&gt;

&lt;p&gt;On Linux, deletion usually removes a directory entry—a name that points to a file’s inode. The file’s stored data can be reclaimed only when two conditions are satisfied:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;No directory entries refer to the inode.&lt;/li&gt;
&lt;li&gt;No process still has the file open.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If a running application opened a log file before it was deleted, its file descriptor remains connected to the same inode. The pathname may have disappeared, but the underlying file still exists for that process. The application can continue writing to it, and the supposedly deleted file may even keep growing.&lt;/p&gt;

&lt;p&gt;This behavior is intentional. Open file descriptors remain valid when files are renamed or unlinked, allowing programs to use files safely without depending on their names remaining unchanged.&lt;/p&gt;

&lt;p&gt;The mismatch between common disk tools makes the problem especially confusing. &lt;code&gt;du&lt;/code&gt; walks the directory tree and totals files it can reach by pathname. An unlinked file has no pathname, so &lt;code&gt;du&lt;/code&gt; cannot see it. &lt;code&gt;df&lt;/code&gt;, by contrast, reports allocated filesystem blocks. Those blocks are still allocated while a process holds the file open. As a result, &lt;code&gt;du&lt;/code&gt; may account for far less data than &lt;code&gt;df&lt;/code&gt; says is in use.&lt;/p&gt;

&lt;p&gt;The usual diagnostic command is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;lsof +L1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This lists open files whose link count is below one, including files marked &lt;code&gt;(deleted)&lt;/code&gt;. The process name, PID, file descriptor, and file size help identify which application is retaining the space.&lt;/p&gt;

&lt;p&gt;The safest remedy is generally to make that application close and reopen the file. Depending on the program, that may mean requesting a log reopen, performing a graceful reload, restarting a service, or restarting a container. Once the final open file descriptor is closed, the kernel can reclaim the blocks.&lt;/p&gt;

&lt;p&gt;Killing a process is not always necessary. Many daemons support log rotation explicitly: the old log is renamed, a signal or reload command asks the daemon to reopen its log path, and the rotated file is then compressed or removed. Without the reopen step, log rotation can leave the daemon writing through its old descriptor.&lt;/p&gt;

&lt;p&gt;There is also an emergency technique: truncating the open descriptor through &lt;code&gt;/proc/&amp;lt;pid&amp;gt;/fd/&amp;lt;fd&amp;gt;&lt;/code&gt;. That can release space without terminating the process, but it should be used cautiously. Writing to another process’s file descriptor can interact badly with buffered output, file offsets, or application assumptions.&lt;/p&gt;

&lt;p&gt;The key distinction is between a filename and an open file. &lt;code&gt;rm&lt;/code&gt; removes the filename. Storage is reclaimed only after the filesystem has neither a remaining name nor a process reference to the file.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>infrastructure</category>
      <category>linux</category>
    </item>
    <item>
      <title>Why Linux Load Average Can Rise While CPUs Stay Mostly Idle</title>
      <dc:creator>Qian_Xiao</dc:creator>
      <pubDate>Wed, 21 Oct 2026 16:04:33 +0000</pubDate>
      <link>https://dev.to/pbxqdown/why-linux-load-average-can-rise-while-cpus-stay-mostly-idle-111d</link>
      <guid>https://dev.to/pbxqdown/why-linux-load-average-can-rise-while-cpus-stay-mostly-idle-111d</guid>
      <description>&lt;p&gt;A high Linux load average does not necessarily mean the processors are busy. The common claim that “load average measures CPU usage” confuses two related but distinct signals.&lt;/p&gt;

&lt;p&gt;Linux calculates load average from the number of tasks that are runnable and the number stuck in uninterruptible sleep. Runnable tasks are executing on a CPU or waiting for one. Tasks in uninterruptible sleep, usually shown as state &lt;code&gt;D&lt;/code&gt; by tools such as &lt;code&gt;ps&lt;/code&gt;, are waiting for a kernel operation to finish and cannot currently handle ordinary signals.&lt;/p&gt;

&lt;p&gt;Storage I/O is a frequent cause of uninterruptible sleep. Suppose many processes read from a slow or unavailable disk. Those processes may spend most of their time waiting rather than executing instructions. CPU utilization can remain low, yet the load average rises because Linux still includes those waiting tasks in its load calculation.&lt;/p&gt;

&lt;p&gt;This distinction explains why load average and CPU utilization must be read separately. CPU utilization reports how much processor time is being consumed. Load average reflects sustained demand from runnable tasks plus certain blocked tasks. It is therefore possible to observe a load average of 20 alongside mostly idle CPUs when many processes are waiting in &lt;code&gt;D&lt;/code&gt; state.&lt;/p&gt;

&lt;p&gt;The three familiar values in &lt;code&gt;/proc/loadavg&lt;/code&gt; and commands such as &lt;code&gt;uptime&lt;/code&gt; are exponentially weighted averages over approximately one, five, and fifteen minutes. They are not instantaneous counts. A short disturbance can keep the one-minute value elevated after the underlying problem has disappeared, while the longer values reveal whether the pressure is persistent.&lt;/p&gt;

&lt;p&gt;CPU count still matters when runnable work is responsible for the load. A load near eight may indicate saturation on an eight-CPU system because roughly eight tasks can run simultaneously. But that interpretation fails if much of the count comes from uninterruptible sleepers. Dividing load average by CPU count does not transform it into a reliable utilization percentage.&lt;/p&gt;

&lt;p&gt;When load is high but CPU usage is low, inspect task states before adding processor capacity. Commands such as &lt;code&gt;ps&lt;/code&gt; can reveal &lt;code&gt;D&lt;/code&gt;-state processes, while storage latency, network-backed filesystems, device failures, and kernel waits may identify the resource actually causing the queue.&lt;/p&gt;

&lt;p&gt;Load average is best treated as a pressure clue. It says that tasks are accumulating, but CPU metrics and process states are needed to determine why.&lt;/p&gt;

</description>
      <category>infrastructure</category>
      <category>linux</category>
      <category>performance</category>
    </item>
    <item>
      <title>Docker Stop Signals Reach PID 1, Not Every Process in the Container</title>
      <dc:creator>Qian_Xiao</dc:creator>
      <pubDate>Fri, 16 Oct 2026 16:57:48 +0000</pubDate>
      <link>https://dev.to/pbxqdown/docker-stop-signals-reach-pid-1-not-every-process-in-the-container-39e0</link>
      <guid>https://dev.to/pbxqdown/docker-stop-signals-reach-pid-1-not-every-process-in-the-container-39e0</guid>
      <description>&lt;p&gt;A common misconception says that &lt;code&gt;docker stop&lt;/code&gt; sends &lt;code&gt;SIGTERM&lt;/code&gt; to every process inside a container. It does not. The signal is sent to the container’s main process: PID 1 in the container’s PID namespace.&lt;/p&gt;

&lt;p&gt;This distinction matters when PID 1 is not the application that needs to shut down gracefully. Consider an image with this instruction:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;ENTRYPOINT&lt;/span&gt;&lt;span class="s"&gt; service --listen&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is Dockerfile shell form. Docker normally starts it through a shell equivalent to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/bin/sh &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'service --listen'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The shell becomes PID 1, while &lt;code&gt;service&lt;/code&gt; runs as its child. When the container is stopped, the runtime signals the shell. Whether the application receives that signal now depends on the shell’s behavior. A non-interactive shell may not forward &lt;code&gt;SIGTERM&lt;/code&gt; to its child. The service can therefore miss its opportunity to stop accepting work, flush buffers, close connections, or persist state.&lt;/p&gt;

&lt;p&gt;After the configured grace period expires, Docker sends &lt;code&gt;SIGKILL&lt;/code&gt; to the container. &lt;code&gt;SIGKILL&lt;/code&gt; cannot be caught or handled, so the application is terminated immediately.&lt;/p&gt;

&lt;p&gt;Exec form removes this unnecessary intermediary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;ENTRYPOINT&lt;/span&gt;&lt;span class="s"&gt; ["service", "--listen"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, &lt;code&gt;service&lt;/code&gt; itself becomes PID 1 and directly receives the stop signal. A wrapper script can also work, but it should replace itself with the application by using &lt;code&gt;exec&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/sh&lt;/span&gt;
prepare_environment
&lt;span class="nb"&gt;exec &lt;/span&gt;service &lt;span class="nt"&gt;--listen&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without &lt;code&gt;exec&lt;/code&gt;, the wrapper remains PID 1 and must explicitly forward signals.&lt;/p&gt;

&lt;p&gt;PID 1 also has a special process-management responsibility. Orphaned descendants are reparented to it, and it must reap exited children to prevent zombie processes. Applications that spawn subprocesses but do not handle this role well may benefit from a small init process such as Docker’s &lt;code&gt;--init&lt;/code&gt; option. That init process can forward signals and reap children while remaining PID 1.&lt;/p&gt;

&lt;p&gt;The precise rule is simple: stopping a container signals its designated main process, not every process in the container. Graceful shutdown depends on ensuring that PID 1 either is the application or correctly forwards signals to it.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>docker</category>
      <category>linux</category>
    </item>
    <item>
      <title>A Failed Kubernetes Readiness Probe Does Not Restart the Container</title>
      <dc:creator>Qian_Xiao</dc:creator>
      <pubDate>Sun, 11 Oct 2026 20:58:27 +0000</pubDate>
      <link>https://dev.to/pbxqdown/a-failed-kubernetes-readiness-probe-does-not-restart-the-container-15fh</link>
      <guid>https://dev.to/pbxqdown/a-failed-kubernetes-readiness-probe-does-not-restart-the-container-15fh</guid>
      <description>&lt;p&gt;A common misconception says that when a Kubernetes readiness probe fails, Kubernetes restarts the container. It does not. Readiness controls whether a Pod should receive traffic from Services; container restarts are the job of liveness probes and process supervision.&lt;/p&gt;

&lt;p&gt;The kubelet runs a container’s readiness probe according to the probe configuration. If the probe succeeds, Kubernetes can mark the container as ready. If every required readiness condition is satisfied, the Pod’s &lt;code&gt;Ready&lt;/code&gt; condition becomes true. When the probe begins failing, the container is marked not ready, which normally makes the Pod not ready as well.&lt;/p&gt;

&lt;p&gt;That state change affects service discovery. Controllers update the relevant EndpointSlices so that a not-ready Pod is no longer treated as an eligible backend for ordinary Service traffic. Components such as &lt;code&gt;kube-proxy&lt;/code&gt;, or another implementation of Service routing, then converge on the new endpoint state.&lt;/p&gt;

&lt;p&gt;The container keeps running throughout this process. Its process is not killed, its filesystem is not reset, and its restart count does not increase merely because readiness failed. The kubelet continues executing the readiness probe. If the application recovers and the probe succeeds again, the Pod can become ready and rejoin the Service’s available backends without a restart.&lt;/p&gt;

&lt;p&gt;This distinction makes readiness probes suitable for temporary inability to serve traffic. An application may be alive but unable to answer requests while it refreshes configuration, reconnects to a dependency, fills a cache, or drains work. A readiness failure can isolate that instance while preserving its in-memory recovery process.&lt;/p&gt;

&lt;p&gt;Removal from Service endpoints is not an instantaneous cancellation mechanism. Endpoint updates must propagate, and existing connections may remain open. A client using a persistent TCP connection might continue sending requests to the same Pod even after that Pod becomes unready. Readiness is therefore a routing signal, not a guarantee that all traffic stops at the exact moment a probe fails.&lt;/p&gt;

&lt;p&gt;If an unhealthy container must be terminated and started again, a liveness probe expresses that policy. A liveness failure can cause the kubelet to restart the container according to the Pod’s restart policy. Using liveness for a condition that can recover naturally may instead create restart loops and erase useful recovery progress.&lt;/p&gt;

&lt;p&gt;The precise correction is simple: readiness failure removes a Pod from normal Service traffic; it does not restart the container.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>kubernetes</category>
      <category>sre</category>
    </item>
    <item>
      <title>A Bind Mount Hides Existing Container Files; It Does Not Merge with Them</title>
      <dc:creator>Qian_Xiao</dc:creator>
      <pubDate>Fri, 09 Oct 2026 19:43:21 +0000</pubDate>
      <link>https://dev.to/pbxqdown/a-bind-mount-hides-existing-container-files-it-does-not-merge-with-them-3dj2</link>
      <guid>https://dev.to/pbxqdown/a-bind-mount-hides-existing-container-files-it-does-not-merge-with-them-3dj2</guid>
      <description>&lt;p&gt;A common misconception about container bind mounts is that mounting a host directory over a container directory combines the contents of both locations. It does not. The mount replaces the directory’s visible view for as long as the mount exists.&lt;/p&gt;

&lt;p&gt;Suppose an image contains these files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/app/config/default.yaml
/app/config/schema.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The container is then started with a host directory mounted at &lt;code&gt;/app/config&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;--mount&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;bind&lt;/span&gt;,src&lt;span class="o"&gt;=&lt;/span&gt;/srv/config,dst&lt;span class="o"&gt;=&lt;/span&gt;/app/config example
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;/srv/config&lt;/code&gt; contains only &lt;code&gt;production.yaml&lt;/code&gt;, the container sees:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/app/config/production.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It does not also see &lt;code&gt;default.yaml&lt;/code&gt; or &lt;code&gt;schema.json&lt;/code&gt;. Those image files have not been deleted, copied, or overwritten. They are merely obscured by the mount.&lt;/p&gt;

&lt;p&gt;This behavior comes from Linux mount semantics rather than a container-specific merge algorithm. A mount attaches one filesystem tree at a mount point. Path lookup beneath that point proceeds through the mounted tree, so the directory entries from the underlying image layer are no longer reachable through their original paths.&lt;/p&gt;

&lt;p&gt;That distinction explains a frequent failure mode: an application works when launched directly from its image, then fails after a configuration directory is mounted. The operator expected to override one file, but mounting the directory hid every image-provided file under that path.&lt;/p&gt;

&lt;p&gt;A bind mount also differs from the image’s layered filesystem. Image layers can contribute files to a combined container root filesystem, but a bind mount is applied afterward at a particular path. It does not become another union layer whose entries are merged with the directory underneath.&lt;/p&gt;

&lt;p&gt;If only one file should be replaced, mount that file instead of its parent directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--mount&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;bind&lt;/span&gt;,src&lt;span class="o"&gt;=&lt;/span&gt;/srv/config/production.yaml,dst&lt;span class="o"&gt;=&lt;/span&gt;/app/config/default.yaml,readonly &lt;span class="se"&gt;\&lt;/span&gt;
  example
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Another option is to place defaults outside the mount point and have an entrypoint copy them into a writable directory before starting the application. The exact design depends on whether configuration should be immutable, generated, or persistent.&lt;/p&gt;

&lt;p&gt;Stopping and removing the container removes the mount relationship, not the underlying image files. A new container started without that bind mount sees the original files again.&lt;/p&gt;

&lt;p&gt;The corrected claim is precise: a bind mount does not merge host and container directory contents. It makes the mounted source the visible filesystem tree at the destination, hiding whatever was previously visible there.&lt;/p&gt;

</description>
      <category>containers</category>
      <category>devops</category>
      <category>docker</category>
    </item>
    <item>
      <title>Was bash the wrong language for my agent?</title>
      <dc:creator>Qian_Xiao</dc:creator>
      <pubDate>Thu, 17 Sep 2026 20:36:47 +0000</pubDate>
      <link>https://dev.to/pbxqdown/was-bash-the-wrong-language-for-my-agent-4i6l</link>
      <guid>https://dev.to/pbxqdown/was-bash-the-wrong-language-for-my-agent-4i6l</guid>
      <description>&lt;p&gt;I have a small agent that handles one piece of routine work at a time. It looks&lt;br&gt;
at what needs doing, picks the thing most worth doing, shows me the plan, and&lt;br&gt;
does it if I say yes. Underneath, it is glue: it drives a few command-line&lt;br&gt;
tools, calls a model, reshapes a lot of JSON, and prints a readable summary.&lt;/p&gt;

&lt;p&gt;It was 2150 lines of bash across seven files. It is now Python.&lt;/p&gt;

&lt;p&gt;So the answer looks like yes. I don't think it is, and why I don't is most of&lt;br&gt;
the reason I'm writing this down.&lt;/p&gt;
&lt;h2&gt;
  
  
  The argument for staying was sound. Its inputs weren't.
&lt;/h2&gt;

&lt;p&gt;I had been through this question before and decided to stay — carefully enough&lt;br&gt;
that the reasoning became a section of the project README titled &lt;strong&gt;Why this is&lt;br&gt;
still bash&lt;/strong&gt;. Nine tenths of the program is subprocess orchestration, which is&lt;br&gt;
bash's home ground. Rewriting 2000 lines with no test coverage is the standard&lt;br&gt;
way to lose behavior silently. And the bug ledger said the expensive bugs were&lt;br&gt;
design errors that any language would have permitted.&lt;/p&gt;

&lt;p&gt;I still think all of that is true.&lt;/p&gt;

&lt;p&gt;What moved was a requirement. I had been treating &lt;em&gt;runs with no build step&lt;/em&gt; as&lt;br&gt;
hard, which made "python3 is already installed" the load-bearing argument. Then&lt;br&gt;
the requirement got clarified: no build step isn't a rule, it just shouldn't be&lt;br&gt;
complicated to start.&lt;/p&gt;

&lt;p&gt;That one sentence killed my best argument. So I measured what was actually at&lt;br&gt;
stake — 89ms for Python plus every standard library module it needs, against&lt;br&gt;
3ms for bash. Eighty-six milliseconds, in a program that waits thirty to a&lt;br&gt;
hundred seconds on a model.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My reasoning was valid; its inputs weren't, and I had spent almost no effort&lt;br&gt;
checking them.&lt;/strong&gt; That ratio was backwards, and I don't think that's unusual.&lt;/p&gt;
&lt;h2&gt;
  
  
  What decided it was 162 calls to &lt;code&gt;jq&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Every list length, every filter, forking a process to handle data that should&lt;br&gt;
have been sitting in memory.&lt;/p&gt;

&lt;p&gt;The cost was not performance. 162 forks are nothing next to a minute of model&lt;br&gt;
latency. &lt;strong&gt;The cost was expressiveness.&lt;/strong&gt; Every structure in the program either&lt;br&gt;
fit in a one-line &lt;code&gt;jq&lt;/code&gt; expression or got split into three pieces. What I wrote&lt;br&gt;
was never the structure I wanted; it was the structure &lt;code&gt;jq&lt;/code&gt; could state on one&lt;br&gt;
line. That cost is invisible in any single line of code and shows up in the&lt;br&gt;
designs you never consider.&lt;/p&gt;

&lt;p&gt;I had three lists: the files a change actually touched, the files the agent&lt;br&gt;
itself had written, and the files I had approved in advance. I needed two&lt;br&gt;
differences between them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;later&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;jq &lt;span class="nt"&gt;-nc&lt;/span&gt; &lt;span class="nt"&gt;--argjson&lt;/span&gt; a &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$actual&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--argjson&lt;/span&gt; w &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$written&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="s1"&gt;'if $w == null then [] else ($a - $w) end'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;extra&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;jq &lt;span class="nt"&gt;-nr&lt;/span&gt; &lt;span class="nt"&gt;--argjson&lt;/span&gt; w &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$written&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--argjson&lt;/span&gt; p &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$planned&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="s1"&gt;'($w - $p) | join(", ")'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;later&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;written&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;actual&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;written&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;extra&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;written&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;planned&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second one is barely shorter. It is what I would have written on the first&lt;br&gt;
attempt; the first took me several tries to get right.&lt;/p&gt;

&lt;p&gt;There was also a run that died on &lt;code&gt;line 567: 1: command not found&lt;/code&gt;, which I&lt;br&gt;
never located. It went away when that section was rewritten for unrelated&lt;br&gt;
reasons. &lt;strong&gt;A bug you can't find after the fact doesn't just go unfixed — it&lt;br&gt;
tells you the next one of its kind will too.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The line count did not go down
&lt;/h2&gt;

&lt;p&gt;The port took a day.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bash      2150 lines
Python    2258 lines   ← up 108

            of which:
  code      1278       ← down 40%
  comments   980
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Most people expect the opposite, so it's worth being blunt about. The code&lt;br&gt;
shrank by forty percent; the difference is comments and docstrings — the notes&lt;br&gt;
recording which specific incident each safety check exists to prevent, which&lt;br&gt;
were exactly what I'd been afraid of losing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Evaluate this rewrite by total line count and it accomplished nothing.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What made it cheap: two seams built for other reasons
&lt;/h2&gt;

&lt;p&gt;This is the part I actually wanted to write down.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The prompts are files, not strings.&lt;/strong&gt; Every prompt lives in its own Markdown&lt;br&gt;
file and the code fills &lt;code&gt;{{placeholder}}&lt;/code&gt; holes in it. I did that for unrelated&lt;br&gt;
reasons: prompts get edited constantly, they want to be read as prose, and a&lt;br&gt;
stray &lt;code&gt;$&lt;/code&gt; or backtick has to stay inert instead of being eaten by the shell.&lt;/p&gt;

&lt;p&gt;The result was that the migration &lt;strong&gt;did not touch one word of any prompt&lt;/strong&gt;. I&lt;br&gt;
checked modification times afterward to be sure. Everything that determines&lt;br&gt;
this program's behavior — the criteria I've tuned over and over, the order&lt;br&gt;
judgments get made in — lives in those files. Changing languages only replaced&lt;br&gt;
the glue that assembles them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Each kind of task is a separate executable that speaks JSON.&lt;/strong&gt; Verb on argv,&lt;br&gt;
JSON on stdin, JSON on stdout. I built it that way because bash has no modules,&lt;br&gt;
and putting them behind a process boundary beat having them scribble on each&lt;br&gt;
other's variables. Pure coping.&lt;/p&gt;

&lt;p&gt;The result: &lt;strong&gt;there was no big-bang rewrite to choose.&lt;/strong&gt; The orchestrator could&lt;br&gt;
be Python while the task types were still bash, or the reverse. I moved one&lt;br&gt;
file at a time and ran each one on its own afterward. (That boundary is&lt;br&gt;
probably also why this never hit the wall bash projects hit — the largest bash&lt;br&gt;
agent I know of reached 4700 lines as a single file assembled by &lt;code&gt;cat src/*.sh&lt;/code&gt;,&lt;br&gt;
and what broke was module structure, not correctness.)&lt;/p&gt;

&lt;p&gt;Neither seam was built with portability in mind. Both were built to solve&lt;br&gt;
something annoying at the time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Whether a rewrite will be cheap is decided before you start it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I kept the process boundary afterward, by the way. Folding the task types into&lt;br&gt;
Python imports would save a JSON round trip, and it is the best structural&lt;br&gt;
decision in the project.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real risk of Python is not the build step
&lt;/h2&gt;

&lt;p&gt;With a shebang and standard library only, it is invoked exactly the way it was&lt;br&gt;
before. No virtualenv, no install.&lt;/p&gt;

&lt;p&gt;The risk is that &lt;strong&gt;Python invites dependencies&lt;/strong&gt;, and bash's poverty was itself&lt;br&gt;
a form of protection. &lt;code&gt;requests&lt;/code&gt; when &lt;code&gt;urllib&lt;/code&gt; is right there; a schema&lt;br&gt;
validator when the model CLI already enforces the schema; an argument parser&lt;br&gt;
for 25 lines of parsing; a formatting library for a display layer that exists.&lt;br&gt;
Each has a plausible case, and after all four "quick to start" is gone.&lt;/p&gt;

&lt;p&gt;So there is one rule in the README now: &lt;strong&gt;standard library only, and say why it&lt;br&gt;
can't be done with it before adding anything.&lt;/strong&gt; The whole program needs six&lt;br&gt;
modules.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I did not verify
&lt;/h2&gt;

&lt;p&gt;I exercised every path after the port, including a full dry run of the&lt;br&gt;
expensive one — isolated checkout, model writes the code, formatter, vet,&lt;br&gt;
build, tests, commit — stopping short of pushing. Fifteen tests on the pure&lt;br&gt;
functions pass.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The two lines that push a branch and open a change request never ran&lt;/strong&gt;,&lt;br&gt;
because running them means actually opening one. And the safety checks inside&lt;br&gt;
the task types I translated by hand, one at a time. I believe I got them right,&lt;br&gt;
and this project still has no test that can prove it.&lt;/p&gt;




&lt;p&gt;I don't think bash was the wrong choice. It carried this to 2150 lines, and for&lt;br&gt;
all of that time I was changing judgment logic rather than fighting the&lt;br&gt;
language. &lt;strong&gt;Its problem was never that it couldn't do the job. Its problem was&lt;br&gt;
that its expressiveness had started deciding my designs.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>bash</category>
      <category>refactoring</category>
      <category>ai</category>
    </item>
    <item>
      <title>Everything was running. The port belonged to the wrong process.</title>
      <dc:creator>Qian_Xiao</dc:creator>
      <pubDate>Fri, 11 Sep 2026 18:24:59 +0000</pubDate>
      <link>https://dev.to/pbxqdown/everything-was-running-the-port-belonged-to-the-wrong-process-3e1i</link>
      <guid>https://dev.to/pbxqdown/everything-was-running-the-port-belonged-to-the-wrong-process-3e1i</guid>
      <description>&lt;p&gt;My browser sat there spinning on "connecting".&lt;/p&gt;

&lt;p&gt;The setup is common enough: &lt;code&gt;x11vnc&lt;/code&gt; shares a screen on port 5900, and&lt;br&gt;
&lt;code&gt;websockify&lt;/code&gt; forwards to it so a browser can connect. I checked the services.&lt;br&gt;
websockify was running. Everything was running. Nothing worked.&lt;/p&gt;

&lt;p&gt;The question I actually needed answered was simple:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who is using port 5900?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's normally &lt;code&gt;ss -ltnp&lt;/code&gt; or &lt;code&gt;lsof -i :5900&lt;/code&gt;, and neither was installed on that&lt;br&gt;
box — which is why I'd written a small tool for this one question:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ portclue 5900
NOT EXPOSED LOCALLY

TCP port 5900

  127.0.0.1:5900/tcp  [NOT_EXPOSED_LOCALLY]
    -&amp;gt; LISTEN             ... bound to 127.0.0.1:5900/tcp
    -&amp;gt; OWNED              PID 1087636 (x11vnc), systemd unit session-1911.scope
    -&amp;gt; LOOPBACK_ONLY      127.0.0.1 is reachable only from this network namespace
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An &lt;code&gt;x11vnc&lt;/code&gt; had the port — just not the one I'd started. It was a leftover from&lt;br&gt;
a session days earlier that had never shut down, so the new one could never get&lt;br&gt;
the port, and websockify had been faithfully forwarding to a dead screen the&lt;br&gt;
whole time.&lt;/p&gt;

&lt;p&gt;(PortClue gave me the PID. &lt;code&gt;ps&lt;/code&gt; is what confirmed the process was far older than&lt;br&gt;
everything around it — the tool doesn't report process age yet.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I reach for it
&lt;/h2&gt;

&lt;p&gt;Same facts &lt;code&gt;ss&lt;/code&gt; would give you, but written out instead of encoded.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;127.0.0.1&lt;/code&gt; isn't a number you have to interpret; it says "reachable only from&lt;br&gt;
this network namespace". On a port bound to &lt;code&gt;0.0.0.0&lt;/code&gt; it says &lt;code&gt;ALL_INTERFACES&lt;/code&gt;,&lt;br&gt;
then reads your nftables or iptables rules to see whether anything is actually&lt;br&gt;
allowed through. If it can't read them, it says &lt;code&gt;UNKNOWN&lt;/code&gt; instead of guessing.&lt;/p&gt;

&lt;p&gt;It gets all of that without &lt;code&gt;ss&lt;/code&gt; or &lt;code&gt;lsof&lt;/code&gt; installed, by asking the kernel&lt;br&gt;
directly.&lt;/p&gt;

&lt;p&gt;It's read-only: it never connects to the port you ask about, and it can't kill&lt;br&gt;
anything. Scope is Linux TCP listeners — that's the whole promise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://raw.githubusercontent.com/pbxqdown/portclue/v0.1.2/scripts/install.sh | sh

portclue        &lt;span class="c"&gt;# everything listening&lt;/span&gt;
portclue 5900   &lt;span class="c"&gt;# one port, explained&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://github.com/pbxqdown/portclue" rel="noopener noreferrer"&gt;https://github.com/pbxqdown/portclue&lt;/a&gt;&lt;/p&gt;

</description>
      <category>linux</category>
      <category>debugging</category>
      <category>devops</category>
      <category>cli</category>
    </item>
  </channel>
</rss>
