2

Is there a way to handle the WM_CLOSE event that Windows sends when taskkill is used on a process without the /F option? ffmpeg was not designed to catch this event and I want to write a nim program that can manage the ffmpeg process and tell it to shut down gracefully when it receives the WM_CLOSE event. I'm very new to the Windows API and nim, so I don't know where to start.

Here is a gif showing the problem with ffmpeg https://i.stack.imgur.com/d5v6l.jpg

If I kill the ffmpeg process forcefully with the /F option it will end in file corruption during encoding/recording.

This is the code I'm using to test this feature out:

import os, times, strutils


let t = epochTime()

proc handler() {.noconv.} =
  echo "Program has run for ", formatFloat(epochTime() - t, precision = 0), " seconds."
  quit(0)

setControlCHook(handler)

while true:
  sleep 500

This doesn't catch the WM_CLOSE event generated by taskkill.

How can I make my nim program catch the WM_CLOSE event?

RattleyCooper
  • 4,997
  • 5
  • 27
  • 43
  • This much looks like an [XY Problem](http://xyproblem.info). – IInspectable Aug 29 '20 at 16:18
  • Unfortunately, when you don't know what you're doing you don't know exactly what the problem is, you can't write a perfect question. That's just reality. Not every stackoverflow question is going to be perfect. I described errors I'm getting and what I've tried, and what I've researched. Not sure what else you expect me to do buddy. – RattleyCooper Aug 29 '20 at 16:40

1 Answers1

2

Problem

If there isn't a window handle associated with a process, it's unable to catch the WM_CLOSE event that taskkill sends when it's used without the /F flag.

Send WM_CLOSE message to a process with no window

taskkill, when used without the /F flag, sends a WM_CLOSE or WM_QUIT message to the window handle associated with the pid you use when calling taskkill

Difference between taskkill and taskkill /f

Note that I'm on Windows 10 so I have to catch WM_CLOSE instead of WM_QUIT

You can test the problem yourself by opening up a command prompt and running ffmpeg, then trying to taskkill /PID {pid} or taskkill /IM ffmpeg.exe. ffmpeg will continue to encode/record and taskkill will tell you it successfully killed the process.

ffmpeg.exe -rtbufsize 150M -f gdigrab -framerate 30 -offset_x 448 -offset_y 240 -video_size 1024x600 -draw_mouse 1 -show_region 1 -i desktop -r 30 -preset ultrafast -tune zerolatency -movflags +faststart output.mp4

Here is a demo of trying to shut down an ffmpeg process with taskkill. The default behavior for taskkill is unhandled because ffmpeg does not create a window handle that can have the WM_CLOSE event sent to it.

https://i.stack.imgur.com/d5v6l.jpg

^ I used my custom ffmpeg wrapper to catch the WM_CLOSE event and shut down ffmpeg gracefully to record this clip.

Solutions

One way to associate a window with any executable on Windows is to use the start command:

start "Your window title" program.exe -option1 -option2

You can then use taskkill to send the WM_CLOSE event, which can be intercepted, but ffmpeg doesn't actually catch the event because it wasn't designed to catch it in the first place.

To solve this in nim, you can use a combination of 3 additional modules to create a window handle, start/monitor a process, and intercept the WM_CLOSE event and write "q" to the ffmpeg process's stdout.

wNim

winim

nimpy

In other versions of windows taskkill sends the WM_QUIT event, so you may need to handle that as well.

Create the window handle with wNim; winim is only used to get access to the WM_CLOSE constant that's used for the wNim event handler.

import os
import wnim
import winim
import nimpy

# Put python3.8.dll into following folder.  It is required by nimpy
# C:/Users/username/AppData/Local/Microsoft/WindowsApps

let app = App()
let frame = Frame(title="ffmpeg", size=(400, 300)) # Size doesn't matter because we never show the frame.

Load a python3.8.dll file using nimpy so you can use the subprocess module from python. (easier to use than the builtin nim multiprocessing library imho)

# This is used to check if ffmpeg has been launched
var running_process = false  

# The process must be a PyObject, not a Process object from nim
var the_proc: PyObject  

# Get reference to the subprocess module
let subprocess = pyImport("subprocess")  

# Get reference to python builtin modules
let py = pyBuiltinsModule()  

# Get a reference to the subprocess PIPE
let pipe = subprocess.PIPE  

Create event handlers. These will handle the app first starting and the WM_CLOSE event in windows.

# We catch the WM_MOVE event and start the process.
# We force the event to trigger when we center 
# the frame and start the app.
frame.connect(WM_MOVE) do (event: wEvent): 
  # Make sure we only start 1 process.
  if running_process == false:
    running_process = true
    
    # Create command and start the process.
    var command_str: string = paramStr(1)
    for i in 2..paramCount():
      command_str = command_str & " " & paramStr(i)
    
    # Use python subprocess module to execute command and set the stdin to the pipe.
    the_proc = subprocess.Popen(command_str, stdin=pipe)

# When WM_CLOSE is triggered from taskkill, write the utf-8 encoded "q"
# to the ffmpeg process
frame.connect(WM_CLOSE) do (event: wEvent):
  var command = "q"
  # Make sure objects are all safe to use
  # They should by python objects. Call the 
  # standard library to create the python objects.
  var pycommand = py.str.encode(py.str(command), "utf-8")

  # With a python process you call `communicate` on 
  # the process when you want it to wait to complete.
  # Since ffmpeg just needs the "q" character we send
  # the utf-8 encoded "q" with the `input` keyword
  discard the_proc.communicate(input=pycommand)
  sleep(1000)

  # Get rid of process and quit.
  discard the_proc.terminate()
  quit(0)

Then all that needs to be done is centering the frame and starting the app.

frame.center()
app.mainLoop()

Remember that this uses the python standard library, so you need to put a python dll into the following folder for nimpy to get access.

C:/Users/username/AppData/Local/Microsoft/WindowsApps

Here is a repository with everything you need if you happen to run into the same issue:

https://github.com/Wykleph/ffmpeg_wrapper

RattleyCooper
  • 4,997
  • 5
  • 27
  • 43
  • That's nonsense. Taskkill doesn't require the target application to have a window. It takes a PID (process ID) as an argument after all. But since you still avoid telling us, what **problem** you are trying to solve, there's nothing we can do to help. – IInspectable Aug 30 '20 at 19:02
  • I worded that poorly, I'll fix it. But the point I'm trying to make isn't nonsense. I'm on Windows 10, so taskkill sends the `WM_CLOSE` event when taskkill is used without the `/F` option. https://stackoverflow.com/questions/32346219/difference-between-taskkill-and-taskkill-f `WM_CLOSE` requires a window to be associated with a program in order to be caught: https://stackoverflow.com/questions/23197669/send-wm-close-message-to-a-process-with-no-window ffmpeg doesn't catch `WM_CLOSE`. The problem was that I needed to catch the `WM_CLOSE` event so I could taskkill and shut down gracefully. – RattleyCooper Aug 31 '20 at 05:39
  • 1
    including a dependency on python just to launch a subprocess? how decadent. great answer though. – shirleyquirk Sep 17 '20 at 21:38
  • @shirleyquirk I know, it feels wrong, but unfortunately I couldn't get nim's subprocesses to work with ffmpeg without ffmpeg hanging. – RattleyCooper Sep 18 '20 at 19:30
  • @DuckPuncher I was able to do it but couldn't simply pipe the output of the subprocess to stdout while allowing me to send that "q" to the process. Also having played with it now I'm not sure that your "q" ends it rather than the ``terminate`` – shirleyquirk Sep 19 '20 at 20:35