Skip to content

ignacio/winapi

 
 

Repository files navigation

winapi A useful Windows API subset for Lua

This module provides some basic tools for working with Windows systems, finding out system resources, and gives you more control over process creation. In this introduction any plain reference is in the winapi table, so that find_window means winapi.find_window. Normally winapi works with the current Windows code page, but can be told to use UTF-8 with @{set_encoding}; interally string operations are in Unicode.

Creating and working with Processes

An irritating fact is that Lua GUI applications (such as IUP or wxLua) cannot use @{os.execute} without the infamous 'flashing black box' of console creation. And @{io.popen} may in fact not work at all.

@{execute} provides a quiet method to call a shell command. It returns the result code (like @{os.execute}) but also any text generated from the command. So for many common applications it will do as a @{io.popen} replacement as well. This function is blocking, but winapi provides more general ways of launching processes in the background and even capturing their output asynchronously. This will be discussed later with @{spawn_process}.

Apart from @{execute}, @{shell_exec} is the Swiss-Army-Knife of Windows process creation. The first parameter is the 'action' or 'verb' to apply to the path; common actions are 'open', 'edit' and 'print'. Notice that these are the actions defined in Explorer (hence the word 'shell'). So to open a document in Word (or whatever application is registered for this extension):

winapi.shell_exec('open','myold.doc')

Or an explorer window for a directory:

winapi.shell_exec('open','\\users\\steve\\lua')

Note that this function launches the process and does not block. The path may be an explicit program to use, and then we can also specify the command-line parameters:

winapi.shell_exec(nil,'scite','wina.lua')

The fourth parameter is the working directory for the process, and the fifth indicates how the program's window is to be opened. For instance, you can open a file in Notepad already minimized:

winapi.shell_exec(nil,'notepad','wina.lua',nil,winapi.SW_MINIMIZE)

For fine control over console programs, use @{spawn_process} - you pass it the command-line, and receive two values; a process object and a file object. You monitor the process with the first, and can read from or write to the second.

> proc,file = winapi.spawn_process 'cmd /c dir /b'
> = file:read()
bonzo.lc
cexport.lua
class1.c
...
> = proc:wait()
userdata: 0000000000539608      OK
> = proc:exit_code()
0

If the command is invalid, then you will get an error message instead:

> = winapi.spawn_process 'frodo'
nil     The system cannot find the file specified.

This is what @{execute} does under the hood, but doing it explicitly gives you more control. For instance, the @{Process:wait} method of the process object can take an optional time-out parameter; if you wait too long for the process, it will return the process object and the string 'TIMEOUT'.

local _,status = proc:wait(500)
if status == 'TIMEOUT' then
  proc:kill()
end

The file object is unfortunately not a Lua file object, since it is not possible to portably re-use the existing Lua implementation without copying large chunks of liolib.c into this library. So @{File:read} grabs what's available, unbuffered. But I feel that it's easy enough for Lua code to parse the result into separate lines, if needed.

Having a @{File:write} method means that, yes, you can capture an interactive process, send it commands and read the result. The caveat is that this process must not buffer standard output. For instance, launch interactive Lua with a command-line like this:

> proc,file = winapi.spawn_process [[lua -e "io.stdout:setvbuf('no')" -i]]
> = file:read()  -- always read the program banner first!
Lua 5.1.4  Copyright (C) 1994-2008 Lua.org, PUC-Rio
>
> = file:write 'print "hello"\n'
14
> = file:read()
hello
>
> proc:kill()

(We also found it necessary in the Lua for Windows project to switch off buffering for using Lua in SciTE)

Note that reading the result also returns the prompt '>', which isn't so obvious if we're running Lua from within Lua itself. It's clearer when using Python:

> proc,file = winapi.spawn_process [[python -i]]
> = file:read()
Python 2.6.2c1 (r262c1:71369, Apr  7 2009, 18:44:00) [MSC v.1500 32 bit (Intel)]
 on win32
Type "help", "copyright", "credits" or "license" for more information.
>>>
> file:write '40+2\n'
> = file:read()
42
>>>

This kind of interactive process capture is fine for a console application, but @{File:read} is blocking and will freeze any GUI program. For this, you use @{File:read_async} which returns the result through a callback.

> file:write '40+2\n'
> file:read_async(function(s) print('++',s) end)
> ++    42
>>>

This can work nicely with Lua coroutines, allowing us to write pseudo-blocking code for interacting with processes.

The process object can provide more useful information:

> = proc:working_size()
200     1380
> = proc:run_times()
0       31

@{Process:get_working_size} gives you a lower and an upper bound on the process memory in kB; @{Process:get_run_times} gives you the time (in milliseconds) spent in the user process and in the kernel. So the time to calculate 40+2 twice is too fast to even register, and it has only spent 31 msec in the system.

It is possible to wait on more than one process at a time. Consider this simple time-wasting script:

for i = 1,1e8 do end

It takes me 0.743 seconds to do this, with stock Lua 5.1. But running two such scripts in parallel is about the same speed (0.776):

require 'winapi'
local t = os.clock()
local P = {}
P[1] = winapi.spawn_process 'lua slow.lua'
P[2] = winapi.spawn_process 'lua slow.lua'
winapi.wait_for_processes(P,true)
print(os.clock() - t)

So my i3 is effectively a two-processor machine; four such processes take 1.325 seconds, just under twice as long. The second parameter means 'wait for all'; like the @{Process:wait} method, it has an optional timeout parameter.

Working with Windows

The ${Windows} object provides methods for querying window properties. For instance, the desktop window fills the whole screen, so to find out the screen dimensions is straightforward:

> = winapi.desktop_window():get_bounds()
1600    900

Finding other windows is best done by iterating over all top-level windows and checking them for some desired property; (@{find_window} is provided for completeness, but you really have to provide the exact window caption for the second parameter.)

@{find_all_windows} returns all windows matching some function. For convenience, two useful matchers are provided, @{make_name_matcher} and @{make_class_matcher}. Once you have a group of related windows, you can do fun things like tile them:

> t = winapi.find_all_windows(winapi.match_name '- SciTE')
> = #t
2
> winapi.tile_windows(winapi.desktop_window(),false,t)

This call needs the parent window (we just use the desktop), whether to tile horizontally, and a table of window objects. There is an optional fourth parameter, which is the bounds to use for the tiling, specified like so {left=0,top=0,right=600,bottom=900}.

With tiling and the ability to hide windows with w:show(winapi.SW_HIDE) it is entirely possible to write a little 'virtual desktop' application.

@{find_window_ex} also uses a matcher function; @{find_window_match} is a shortcut for the operation of finding a window by its caption.

Every window has an associated text value. For top-level windows, this is the window caption:

> = winapi.foreground_window()
Command Prompt - lua -lwinapi

So the equivalent of the old DOS command title would here be:

winapi.foreground_window():set_text 'My new title'

Any top-level window will contain child windows. For example, Notepad has a simple structure revealed by @{Window:enum_children}:

> w = winapi.find_window_match 'Notepad'
> = w
Untitled - Notepad
> t = {}
> w:enum_children(function(w) table.insert(t,w) end)
> = #t
2
> = t[1]:get_class_name()
Edit
> = t[2]:get_class_name()
msctls_statusbar32

Windows controls like the 'Edit' control interact with the unverse via messages. EM_GETLINECOUNT will tell the control to return the number of lines. Looking up the numerical value of this message, it's easy to query Notepad's edit control:

> = t[1]:send_message(186,0,0)
6

An entertaining way to automate some programs is to send virtual keystrokes to them. The function @{send_to_window} sends characters to the current foreground window:

> winapi.send_input '= 20 + 10\n'
> = 20 + 10
30

After launching a window, you can make it the foreground window and send it text:

winapi.shell_exec(nil,'notepad')
winapi.sleep(100)
notew = winapi.find_window_match 'Untitled'
notew:set_foreground()
winapi.send_input 'Hello World!'

The little sleep is important: it gives the other process a chance to get going, and to create a new window which we can promote.

An important point is that you can choose to use UTF-8 encoding with winapi. This little program shows how:

local W = require 'winapi'
W.set_encoding(W.CP_UTF8)
win = W.foreground_window()
win:set_text 'ελληνική'

When run in SciTE, it successfully puts a little bit of Greek in the title bar.

Working with Processes

@{get_current_process} will give you a @{Process} object for the current program. It's also possible to get a process object from a program's window:

> w = winapi.foreground_window()
> = w
Command Prompt - lua -lwinapi
> p = w:get_process()
> = p:get_process_name()
cmd.exe
> = p:get_process_name(true)
C:\WINDOWS\system32\cmd.exe

(Note that the @{Process:get_process_name} method can optionally give you the full path to the process.)

To get all the current processes:

pids = winapi.get_processes()

for _,pid in ipairs(pids) do
   local P = winapi.process(pid)
   local name = P:get_process_name(true)
   if name then print(pid,name) end
   P:close()
end

Drive and Directory Operations

There are functions for querying the filesystem: @{get_logical_drives} returns all available drives (in 'D:\' format) and @{get_drive_type} will tell you whether these drives are fixed, remote, removable, etc. @{get_disk_free_space} will return the space used and the space available in kB as two results.

require 'winapi'

drives = winapi.get_logical_drives()
for _,drive in ipairs(drives) do
    local free,avail = winapi.get_disk_free_space(drive)
    if not free then -- call failed, avail is error
        free = '('..avail..')'
    else
        free = math.ceil(free/1024) -- get Mb
    end
    local rname = ''
    local dtype = winapi.get_drive_type(drive)
    if dtype == 'remote' then  -- note it wants the drive letter!
        rname = winapi.get_disk_network_name(drive:gsub('\\$',''))
    end
    print(drive,dtype,free,rname)
end

This script gives the following output on my home machine:

C:\	fixed	218967
F:\	fixed	1517
G:\	cdrom	(The device is not ready.)
Q:\	fixed	(Access is denied.)

Or at work:

C:\	fixed	1455
D:\	fixed	49996
E:\	cdrom	(The device is not ready.)
G:\	remote	33844	\\CARL-VFILE\SYS
I:\	remote	452789	\\CARL-VFILE\GROUPS
X:\	remote	12160	\\CARL-VFILE\APPS
Y:\	remote	33844	\\CARL-VFILE\SYS\PUBLIC
Z:\	remote	33844	\\CARL-VFILE\SYS\PUBLIC

A useful operation is watching directories for changes. You specify the directory, the kind of change to monitor and whether subdirectories should be checked. You also provide a function that will be called when something changes.

winapi.watch_for_file_changes(mydir,winapi.FILE_NOTIFY_CHANGE_LAST_WRITE,FALSE,
    function(what,who)
        -- 'what' will be winapi.FILE_ACTION_MODIFIED
        -- 'who' will be the name of the file that changed
        print(what,who)
    end
)

Using a callback means that you can watch multiple directories and still respond to timers, etc.

Finally, @{copy_file} and @{move_file} are indispensible operations which are surprisingly tricky to write correctly in pure Lua. For general filesystem operations like finding the contents of folders, I suggest a more portable library like LuaFileSystem. However, you can get pretty far with a well-behaved way to call system commands:

local status,output = winapi.execute('dir /B')
local files = {}
for f in output:gmatch '[^\r\n]+' do
    table.insert(files,f)
end

Output and Timers

GUI applications do not have a console so @{print} does not work. @{show_message} will put up a message box to bother users, and @{output_debug_string} will write text quietly to the debug stream. A utility such as DebugView can be used to view this output, which shows it with a timestamp.

Here is the old favourite, system message boxes:

print(winapi.show_message("Message","stuff\nand nonsense","yes-no","warning"))

The first parameter is the caption of the message box, the second is the text (which may contain line feeds); the third controls which buttons are to be shown, and the fourth is the icon to use. The function returns a string indicating which button has been pressed: 'ok','yes','no','cancel', etc.

Or you may prefer to irritate the user with a sound:

winapi.beep 'warning'

It is straightforward to create a timer. You could of course use @{sleep} but then your application will do nothing but sleep most of the time. This callback-driven timer can run in the background:

winapi.make_timer(500,function()
    text:append 'gotcha'
end)

Such callbacks can be made GUI-safe by first calling @{use_gui} which ensures that any callback is called in the main GUI thread.

The basic rule for callbacks enforced by winapi is that only one may be active at a time; otherwise we would risk re-entering Lua using the same state. So be quick when responding to callbacks, since they effectively block Lua. For a console application, the best bet (after setting some timers and so forth) is just to sleep indefinitely:

winapi.sleep(-1)

To show what happens if you don't follow the rule:

> winapi.timer(500,function() end)
> = 23
nil     nil     return  23

In short: completely messed!

Reading from the Registry

Pipe Server

Interprocess communication (IPC) is one of those tangled, operating-system-dependent things that can be terribly useful. On Unix, named pipes are special files which can be used for two processes to easily exchange information. One process opens the pipe for reading, and the other process opens it for writing; the first process will start reading, and this will block until the other process writes to the pipe. Since pipes are a regular part of the filesystem, two Lua scripts can use regular I/O to complete this transaction.

Life is more complicated on Windows (as usual) but with a little bit of help from the API you can get the equivalent mechanism from Windows named pipes. They do work a little differently; they are more like Unix domain sockets; a server waits for a client to connect ('accept') and then produces a handle for the new client to use; it then goes back to waiting for connections.

require 'winapi'

winapi.server(function(file)
  file:read_async(function(s) print('['..s..']') end)
end)

winapi.sleep(-1)

Like timers and file notifications, this server runs in its own thread so we have to put the main thread to sleep. This function is passed a callback and a pipe name; pipe names must look like '\\.\pipe\NAME' and the default name is '\\.\pipe\luawinapi'. The callback receives a file object - in this case we use @{File:read_async} to play nice with other Lua threads. Multiple clients can have open connections in this way, up to the number of available pipes.

The client can connect in a very straightforward way:

> f = io.open('\\\\.\\pipe\\luawinapi','w')
> f:write 'hello server!\n'
> f:flush()
> f:close()

and our server will say:

[hello server!
]
[]

(Note that @{File:read} receives an empty string when the handle is closed.)

However, we can't push 'standard' I/O very far here. So there is also a corresponding @{open_pipe} which returns a file object, both readable and writeable. It's probably best to think of it as a kind of socket; each call to @{File:read} and @{File:write} are regarded as receive/send events.

The server can do something to the received string and pass it back:

winapi.server(function(file)
  file:read_async(function(s)
   if s == '' then
     print 'client disconnected'
   else
    file:write (s:upper())
   end
 end)
end)

On the client side:

f = winapi.open_pipe()
f:write 'hello\n'
print(f:read()) -- HELLO
f:write 'dog\n'
print(f:read()) -- DOG
f:close()

Another similarity with sockets is that you can connect to remote pipes (see pipe names)

About

Minimal but useful Lua bindings to the Windows API

Resources

Stars

Watchers

Forks

Packages

No packages published

Languages

  • C 69.0%
  • Lua 30.3%
  • Shell 0.7%