Skip to content

Build1 publisher2 min readPublished

A port file lets the next launch tell a hung instance from a healthy one

A Flask app packaged by PyInstaller kept running after users quit, because os._exit(0) fired from a daemon thread does not reliably kill a frozen build. The cleanup now happens at the next startup.

The Engineer · Build desk

Illustration accompanying A port file lets the next launch tell a hung instance from a healthy one

What happened

  • A dev.to write-up documents a Python desktop app with a Flask backend shown in a browser and packaged by PyInstaller into a single executable, whose process kept running after the user quit.
  • The running instance records its own information in a file named app_running.port, written as the port on one line and the PID on the next.
  • Startup then connects to the recorded port on 127.0.0.1 with a one-second timeout, and force-kills only when the PID is alive and the port fails to respond.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

  • decision Moving the kill to startup means an unreliable shutdown path can stay in the shipped build, and the next launch does the cleanup the quit button failed to do.
  • constraint The second layer only exists for an app that publishes a port it answers on; a packaged app whose background worker has no listening socket gives startup nothing to probe, so it is back to trusting the PID.
  • cost Portability is paid for in duplicate code, since each check needs one implementation per platform; verify the fix on a Mac and the Windows branch is still untested.

The post attributes the leftover process to os._exit(0) called from a background daemon thread, and it does not explain why that call fails in a frozen build [22]. A call whose entire purpose is to end the process immediately sometimes does not [3]. Daemon threads are the ones forcibly terminated when the main thread exits [4], so the quit path asks a disposable thread to take the whole process down with it. The post documents the workaround: detect the leftover process at the next startup, clean it up, and only then start fresh [20].

The liveness check is the careful part of the design. On Unix-like systems it calls os.kill(pid, 0), and POSIX treats signal 0 as a special case that delivers nothing and performs only the permission and existence checks [8]. A PID nobody owns raises ProcessLookupError, a subclass of OSError [9]. Windows has no equivalent, so the code asks kernel32 for a handle via OpenProcess with SYNCHRONIZE rights, 0x00100000, closes it again, and treats the success of the open as the answer [10].

A live PID can belong to a hung app. The post's argument is that a process can be alive while its main loop is hung or its listening socket has stopped responding. From outside, that is indistinguishable from an unresponsive zombie [11]. So startup opens a TCP connection to 127.0.0.1 on the recorded port using connect_ex with a one-second timeout, and reads a return value of 0 as listening [12]. Of the three states that come out, exactly one calls the force-kill: a live PID whose port stays silent [13][17]. An instance that answers is left running with its port file untouched [13], because killing it would take down an app the user never asked to stop [14].

Identity in this design is a number in a file. Both checks ask whether something holds the recorded PID, not whether that something is the app [19]. When the port does not answer, the Windows path runs taskkill /F /PID against that number with its output discarded [15]. Whatever holds the number is what gets killed [19].

settimeout(1.0) caps the probe at one second, and one second is the ceiling on what this handshake adds to a cold start [18]. Both helper functions carry macOS and Windows in their docstrings [23].

What to watch

  • Whether a PyInstaller issue report pins down why os._exit(0) from a daemon thread leaves a frozen build running.
  • Whether the port file gains an identity token, such as process start time, so a reused PID cannot be force-killed by mistake.
  • Whether the Unix branch of both helpers is tested on Linux, since the docstrings name only macOS and Windows.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories