From b9d1e8848fb4f8e45796a706a0391734b0c88c7c Mon Sep 17 00:00:00 2001 From: Karl Berry Date: Tue, 30 Apr 2019 22:36:50 +0000 Subject: cluttex (30apr19) git-svn-id: svn://tug.org/texlive/trunk@50913 c570f23f-e606-0410-a88d-b1316a301751 --- Master/texmf-dist/scripts/cluttex/cluttex.lua | 532 +++++++++++++++++++++++++- 1 file changed, 515 insertions(+), 17 deletions(-) (limited to 'Master/texmf-dist/scripts/cluttex/cluttex.lua') diff --git a/Master/texmf-dist/scripts/cluttex/cluttex.lua b/Master/texmf-dist/scripts/cluttex/cluttex.lua index dc622725bee..dfbd5c5694f 100755 --- a/Master/texmf-dist/scripts/cluttex/cluttex.lua +++ b/Master/texmf-dist/scripts/cluttex/cluttex.lua @@ -344,7 +344,7 @@ end if os.type == "windows" then package.preload["texrunner.shellutil"] = function(...) --[[ - Copyright 2016 ARATA Mizuki + Copyright 2016,2019 ARATA Mizuki This file is part of ClutTeX. @@ -363,6 +363,7 @@ package.preload["texrunner.shellutil"] = function(...) ]] local string_gsub = string.gsub +local os_execute = os.execute -- s: string local function escape(s) @@ -370,14 +371,21 @@ local function escape(s) end +local function has_command(name) + local result = os_execute("where " .. escape(name) .. " > NUL 2>&1") + -- Note that os.execute returns a number on Lua 5.1 or LuaTeX + return result == 0 or result == true +end + return { escape = escape, + has_command = has_command, } end else package.preload["texrunner.shellutil"] = function(...) --[[ - Copyright 2016 ARATA Mizuki + Copyright 2016,2019 ARATA Mizuki This file is part of ClutTeX. @@ -400,6 +408,7 @@ local string_match = string.match local table = table local table_insert = table.insert local table_concat = table.concat +local os_execute = os.execute -- s: string local function escape(s) @@ -430,8 +439,15 @@ local function escape(s) end +local function has_command(name) + local result = os_execute("which " .. escape(name) .. " > /dev/null") + -- Note that os.execute returns a number on Lua 5.1 or LuaTeX + return result == 0 or result == true +end + return { escape = escape, + has_command = has_command, } end end @@ -542,6 +558,7 @@ package.preload["texrunner.option"] = function(...) -- options_and_params, i = parseoption(arg, options) -- options[i] = {short = "o", long = "option" [, param = true] [, boolean = true] [, allow_single_hyphen = false]} +-- options_and_params[j] = {"option", "value"} -- arg[i], arg[i + 1], ..., arg[#arg] are non-options local function parseoption(arg, options) local i = 1 @@ -581,6 +598,7 @@ local function parseoption(arg, options) elseif o.boolean and name == "no-" .. o.long then -- --no-option opt = o + param = false break end end @@ -620,6 +638,7 @@ local function parseoption(arg, options) elseif o.boolean and name == "no-" .. o.long then -- -no-option opt = o + param = false break end elseif o.long and #name >= 2 and (o.long == name or (o.boolean and name == "no-" .. o.long)) then @@ -2124,6 +2143,423 @@ return { info = info_msg, } end +package.preload["texrunner.fswatcher_windows"] = function(...) +--[[ + Copyright 2019 ARATA Mizuki + + This file is part of ClutTeX. + + ClutTeX is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + ClutTeX is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with ClutTeX. If not, see . +]] + +local ffi = require "ffi" +local bitlib = assert(bit32 or bit, "Neither bit32 (Lua 5.2) nor bit (LuaJIT) found") -- Lua 5.2 or LuaJIT + +ffi.cdef[[ +typedef int BOOL; +typedef unsigned int UINT; +typedef uint32_t DWORD; +typedef void *HANDLE; +typedef uintptr_t ULONG_PTR; +typedef uint16_t WCHAR; +typedef struct _OVERLAPPED { + ULONG_PTR Internal; + ULONG_PTR InternalHigh; + union { + struct { + DWORD Offset; + DWORD OffsetHigh; + }; + void *Pointer; + }; + HANDLE hEvent; +} OVERLAPPED; +typedef struct _FILE_NOTIFY_INFORMATION { + DWORD NextEntryOffset; + DWORD Action; + DWORD FileNameLength; + WCHAR FileName[?]; +} FILE_NOTIFY_INFORMATION; +typedef void (__stdcall *LPOVERLAPPED_COMPLETION_ROUTINE)(DWORD dwErrorCode, DWORD dwNumberOfBytesTransfered, OVERLAPPED *lpOverlapped); +DWORD GetLastError(); +BOOL CloseHandle(HANDLE hObject); +HANDLE CreateFileA(const char *lpFileName, DWORD dwDesiredAccess, DWORD dwShareMode, void *lpSecurityAttributes, DWORD dwCreationDisposition, DWORD dwFlagsAndAttributes, HANDLE hTemplateFile); +HANDLE CreateIoCompletionPort(HANDLE fileHandle, HANDLE existingCompletionPort, ULONG_PTR completionKey, DWORD numberOfConcurrentThreads); +BOOL ReadDirectoryChangesW(HANDLE hDirectory, void *lpBuffer, DWORD nBufferLength, BOOL bWatchSubtree, DWORD dwNotifyFilter, DWORD *lpBytesReturned, OVERLAPPED *lpOverlapped, LPOVERLAPPED_COMPLETION_ROUTINE lpOverlappedCompletionRoutine); +BOOL GetQueuedCompletionStatus(HANDLE CompletionPort, DWORD *lpNumberOfBytes, ULONG_PTR *lpCompletionKey, OVERLAPPED **lpOverlapped, DWORD dwMilliseconds); +int MultiByteToWideChar(UINT CodePage, DWORD dwFlags, const char *lpMultiByteStr, int cbMultiByte, WCHAR *lpWideCharStr, int cchWideChar); +int WideCharToMultiByte(UINT CodePage, DWORD dwFlags, const WCHAR *lpWideCharStr, int cchWideChar, char *lpMultiByteStr, int cbMultiByte, const char *lpDefaultChar, BOOL *lpUsedDefaultChar); +DWORD GetFullPathNameA(const char *lpFileName, DWORD nBufferLength, char *lpBuffer, char **lpFilePart); +uint64_t GetTickCount64(); +]] + +-- LuaTeX's FFI does not equate a null pointer with nil. +-- On LuaJIT, ffi.NULL is just nil. +local NULL = ffi.NULL + +-- GetLastError +local ERROR_FILE_NOT_FOUND = 0x0002 +local ERROR_PATH_NOT_FOUND = 0x0003 +local ERROR_ACCESS_DENIED = 0x0005 +local ERROR_INVALID_PARAMETER = 0x0057 +local ERROR_INSUFFICIENT_BUFFER = 0x007A +local WAIT_TIMEOUT = 0x0102 +local ERROR_ABANDONED_WAIT_0 = 0x02DF +local ERROR_NOACCESS = 0x03E6 +local ERROR_INVALID_FLAGS = 0x03EC +local ERROR_NOTIFY_ENUM_DIR = 0x03FE +local ERROR_NO_UNICODE_TRANSLATION = 0x0459 +local KnownErrors = { + [ERROR_FILE_NOT_FOUND] = "ERROR_FILE_NOT_FOUND", + [ERROR_PATH_NOT_FOUND] = "ERROR_PATH_NOT_FOUND", + [ERROR_ACCESS_DENIED] = "ERROR_ACCESS_DENIED", + [ERROR_INVALID_PARAMETER] = "ERROR_INVALID_PARAMETER", + [ERROR_INSUFFICIENT_BUFFER] = "ERROR_INSUFFICIENT_BUFFER", + [ERROR_ABANDONED_WAIT_0] = "ERROR_ABANDONED_WAIT_0", + [ERROR_NOACCESS] = "ERROR_NOACCESS", + [ERROR_INVALID_FLAGS] = "ERROR_INVALID_FLAGS", + [ERROR_NOTIFY_ENUM_DIR] = "ERROR_NOTIFY_ENUM_DIR", + [ERROR_NO_UNICODE_TRANSLATION] = "ERROR_NO_UNICODE_TRANSLATION", +} + +-- CreateFile +local FILE_FLAG_BACKUP_SEMANTICS = 0x02000000 +local FILE_FLAG_OVERLAPPED = 0x40000000 +local OPEN_EXISTING = 3 +local FILE_SHARE_READ = 0x00000001 +local FILE_SHARE_WRITE = 0x00000002 +local FILE_SHARE_DELETE = 0x00000004 +local FILE_LIST_DIRECTORY = 0x1 +local INVALID_HANDLE_VALUE = ffi.cast("void *", -1) + +-- ReadDirectoryChangesW / FILE_NOTIFY_INFORMATION +local FILE_NOTIFY_CHANGE_FILE_NAME = 0x00000001 +local FILE_NOTIFY_CHANGE_DIR_NAME = 0x00000002 +local FILE_NOTIFY_CHANGE_ATTRIBUTES = 0x00000004 +local FILE_NOTIFY_CHANGE_SIZE = 0x00000008 +local FILE_NOTIFY_CHANGE_LAST_WRITE = 0x00000010 +local FILE_NOTIFY_CHANGE_LAST_ACCESS = 0x00000020 +local FILE_NOTIFY_CHANGE_CREATION = 0x00000040 +local FILE_NOTIFY_CHANGE_SECURITY = 0x00000100 +local FILE_ACTION_ADDED = 0x00000001 +local FILE_ACTION_REMOVED = 0x00000002 +local FILE_ACTION_MODIFIED = 0x00000003 +local FILE_ACTION_RENAMED_OLD_NAME = 0x00000004 +local FILE_ACTION_RENAMED_NEW_NAME = 0x00000005 + +-- WideCharToMultiByte / MultiByteToWideChar +local CP_ACP = 0 +local CP_UTF8 = 65001 + +local C = ffi.C + +local function format_error(name, lasterror, extra) + local errorname = KnownErrors[lasterror] or string.format("error code %d", lasterror) + if extra then + return string.format("%s failed with %s (0x%04x) [%s]", name, errorname, lasterror, extra) + else + return string.format("%s failed with %s (0x%04x)", name, errorname, lasterror) + end +end +local function wcs_to_mbs(wstr, wstrlen, codepage) + -- wstr: FFI uint16_t[?] + -- wstrlen: length of wstr, or -1 if NUL-terminated + if wstrlen == 0 then + return "" + end + codepage = codepage or CP_ACP + local dwFlags = 0 + local result = C.WideCharToMultiByte(codepage, dwFlags, wstr, wstrlen, nil, 0, nil, nil) + if result <= 0 then + -- Failed + local lasterror = C.GetLastError() + -- Candidates: ERROR_INSUFFICIENT_BUFFER, ERROR_INVALID_FLAGS, ERROR_INVALID_PARAMETER, ERROR_NO_UNICODE_TRANSLATION + return nil, format_error("WideCharToMultiByte", lasterror) + end + local mbsbuf = ffi.new("char[?]", result) + result = C.WideCharToMultiByte(codepage, dwFlags, wstr, wstrlen, mbsbuf, result, nil, nil) + if result <= 0 then + -- Failed + local lasterror = C.GetLastError() + -- Candidates: ERROR_INSUFFICIENT_BUFFER, ERROR_INVALID_FLAGS, ERROR_INVALID_PARAMETER, ERROR_NO_UNICODE_TRANSLATION + return nil, format_error("WideCharToMultiByte", lasterror) + end + return ffi.string(mbsbuf, result) +end +local function mbs_to_wcs(str, codepage) + -- str: Lua string + if str == "" then + return ffi.new("WCHAR[0]") + end + codepage = codepage or CP_ACP + local dwFlags = 0 + local result = C.MultiByteToWideChar(codepage, dwFlags, str, #str, nil, 0) + if result <= 0 then + local lasterror = C.GetLastError() + -- ERROR_INSUFFICIENT_BUFFER, ERROR_INVALID_FLAGS, ERROR_INVALID_PARAMETER, ERROR_NO_UNICODE_TRANSLATION + return nil, format_error("MultiByteToWideChar", lasterror) + end + local wcsbuf = ffi.new("WCHAR[?]", result) + result = C.MultiByteToWideChar(codepage, dwFlags, str, #str, wcsbuf, result) + if result <= 0 then + local lasterror = C.GetLastError() + return nil, format_error("MultiByteToWideChar", lasterror) + end + return wcsbuf, result +end + + +local function get_full_path_name(filename) + local bufsize = 1024 + local buffer + local filePartPtr = ffi.new("char*[1]") + local result + repeat + buffer = ffi.new("char[?]", bufsize) + result = C.GetFullPathNameA(filename, bufsize, buffer, filePartPtr) + if result == 0 then + local lasterror = C.GetLastError() + return nil, format_error("GetFullPathNameA", lasterror, filename) + elseif bufsize < result then + -- result: buffer size required to hold the path + terminating NUL + bufsize = result + end + until result < bufsize + local fullpath = ffi.string(buffer, result) + local filePart = ffi.string(filePartPtr[0]) + local dirPart = ffi.string(buffer, ffi.cast("intptr_t", filePartPtr[0]) - ffi.cast("intptr_t", buffer)) -- LuaTeX's FFI doesn't support pointer subtraction + return fullpath, filePart, dirPart +end + +--[[ + dirwatche.dirname : string + dirwatcher._rawhandle : cdata HANDLE + dirwatcher._overlapped : cdata OVERLAPPED + dirwatcher._buffer : cdata char[?] +]] +local dirwatcher_meta = {} +dirwatcher_meta.__index = dirwatcher_meta +function dirwatcher_meta:close() + if self._rawhandle ~= nil then + C.CloseHandle(ffi.gc(self._rawhandle, nil)) + self._rawhandle = nil + end +end +local function open_directory(dirname) + local dwShareMode = bitlib.bor(FILE_SHARE_READ, FILE_SHARE_WRITE, FILE_SHARE_DELETE) + local dwFlagsAndAttributes = bitlib.bor(FILE_FLAG_BACKUP_SEMANTICS, FILE_FLAG_OVERLAPPED) + local handle = C.CreateFileA(dirname, FILE_LIST_DIRECTORY, dwShareMode, nil, OPEN_EXISTING, dwFlagsAndAttributes, nil) + if handle == INVALID_HANDLE_VALUE then + local lasterror = C.GetLastError() + print("Failed to open "..dirname) + return nil, format_error("CreateFileA", lasterror, dirname) + end + return setmetatable({ + dirname = dirname, + _rawhandle = ffi.gc(handle, C.CloseHandle), + _overlapped = ffi.new("OVERLAPPED"), + _buffer = ffi.new("char[?]", 1024), + }, dirwatcher_meta) +end +function dirwatcher_meta:start_watch(watchSubtree) + local dwNotifyFilter = bitlib.bor(FILE_NOTIFY_CHANGE_FILE_NAME, FILE_NOTIFY_CHANGE_DIR_NAME, FILE_NOTIFY_CHANGE_ATTRIBUTES, FILE_NOTIFY_CHANGE_SIZE, FILE_NOTIFY_CHANGE_LAST_WRITE, FILE_NOTIFY_CHANGE_LAST_ACCESS, FILE_NOTIFY_CHANGE_CREATION, FILE_NOTIFY_CHANGE_SECURITY) + local buffer = self._buffer + local bufferSize = ffi.sizeof(buffer) + local result = C.ReadDirectoryChangesW(self._rawhandle, buffer, bufferSize, watchSubtree, dwNotifyFilter, nil, self._overlapped, nil) + if result == 0 then + local lasterror = C.GetLastError() + return nil, format_error("ReadDirectoryChangesW", lasterror, self.dirname) + end + return true +end +local ActionTable = { + [FILE_ACTION_ADDED] = "added", + [FILE_ACTION_REMOVED] = "removed", + [FILE_ACTION_MODIFIED] = "modified", + [FILE_ACTION_RENAMED_OLD_NAME] = "rename_from", + [FILE_ACTION_RENAMED_NEW_NAME] = "rename_to", +} +function dirwatcher_meta:process(numberOfBytes) + -- self._buffer received `numberOfBytes` bytes + local buffer = self._buffer + numberOfBytes = math.min(numberOfBytes, ffi.sizeof(buffer)) + local ptr = ffi.cast("char *", buffer) + local structSize = ffi.sizeof("FILE_NOTIFY_INFORMATION", 1) + local t = {} + while numberOfBytes >= structSize do + local notifyInfo = ffi.cast("FILE_NOTIFY_INFORMATION*", ptr) + local nextEntryOffset = notifyInfo.NextEntryOffset + local action = notifyInfo.Action + local fileNameLength = notifyInfo.FileNameLength + local fileName = notifyInfo.FileName + local u = { action = ActionTable[action], filename = wcs_to_mbs(fileName, fileNameLength / 2) } + table.insert(t, u) + if nextEntryOffset == 0 or numberOfBytes <= nextEntryOffset then + break + end + numberOfBytes = numberOfBytes - nextEntryOffset + ptr = ptr + nextEntryOffset + end + return t +end + +--[[ + watcher._rawport : cdata HANDLE + watcher._pending : array of { + action = ..., filename = ... + } + watcher._directories[dirname] = { + dir = directory watcher, + dirname = dirname, + files = { [filename] = user-supplied path } -- files to watch + } + watcher[i] = i-th directory (_directories[dirname] for some dirname) +]] + +local fswatcher_meta = {} +fswatcher_meta.__index = fswatcher_meta +local function new_watcher() + local port = C.CreateIoCompletionPort(INVALID_HANDLE_VALUE, nil, 0, 0) + if port == NULL then + local lasterror = C.GetLastError() + return nil, format_error("CreateIoCompletionPort", lasterror) + end + return setmetatable({ + _rawport = ffi.gc(port, C.CloseHandle), -- ? + _pending = {}, + _directories = {}, + }, fswatcher_meta) +end +local function add_directory(self, dirname) + local t = self._directories[dirname] + if not t then + local dirwatcher, err = open_directory(dirname) + if not dirwatcher then + return dirwatcher, err + end + t = { dirwatcher = dirwatcher, dirname = dirname, files = {} } + table.insert(self, t) + local i = #self + local result = C.CreateIoCompletionPort(dirwatcher._rawhandle, self._rawport, i, 0) + if result == NULL then + local lasterror = C.GetLastError() + return nil, format_error("CreateIoCompletionPort", lasterror, dirname) + end + self._directories[dirname] = t + local result, err = dirwatcher:start_watch(false) + if not result then + return result, err + end + end + return t +end +function fswatcher_meta:add_file(path, ...) + local fullpath, filename, dirname = get_full_path_name(path) + local t, err = add_directory(self, dirname) + if not t then + return t, err + end + t.files[filename] = path + return true +end +local INFINITE = 0xFFFFFFFF +local function get_queued(self, timeout) + local startTime = C.GetTickCount64() + local timeout_ms + if timeout == nil then + timeout_ms = INFINITE + else + timeout_ms = timeout * 1000 + end + local numberOfBytesPtr = ffi.new("DWORD[1]") + local completionKeyPtr = ffi.new("ULONG_PTR[1]") + local lpOverlapped = ffi.new("OVERLAPPED*[1]") + repeat + local result = C.GetQueuedCompletionStatus(self._rawport, numberOfBytesPtr, completionKeyPtr, lpOverlapped, timeout_ms) + if result == 0 then + local lasterror = C.GetLastError() + if lasterror == WAIT_TIMEOUT then + return nil, "timeout" + else + return nil, format_error("GetQueuedCompletionStatus", lasterror) + end + end + local numberOfBytes = numberOfBytesPtr[0] + local completionKey = tonumber(completionKeyPtr[0]) + local dir_t = assert(self[completionKey], "invalid completion key: " .. tostring(completionKey)) + local t = dir_t.dirwatcher:process(numberOfBytes) + dir_t.dirwatcher:start_watch(false) + local found = false + for i,v in ipairs(t) do + local path = dir_t.files[v.filename] + if path then + found = true + table.insert(self._pending, {path = path, action = v.action}) + end + end + if found then + return true + end + if timeout_ms ~= INFINITE then + local tt = C.GetTickCount64() + timeout_ms = timeout_ms - (tt - startTime) + startTime = tt + end + until timeout_ms < 0 + return nil, "timeout" +end +function fswatcher_meta:next(timeout) + if #self._pending > 0 then + local result = table.remove(self._pending, 1) + get_queued(self, 0) -- ignore error + return result + else + local result, err = get_queued(self, timeout) + if result == nil then + return nil, err + end + return table.remove(self._pending, 1) + end +end +function fswatcher_meta:close() + if self._rawport ~= nil then + for i,v in ipairs(self) do + v.dirwatcher:close() + end + C.CloseHandle(ffi.gc(self._rawport, nil)) + self._rawport = nil + end +end +--[[ +local watcher = require("fswatcher_windows").new() +assert(watcher:add_file("rdc-sync.c")) +assert(watcher:add_file("sub2/hoge")) +for i = 1, 10 do + local result, err = watcher:next(2) + if err == "timeout" then + print(os.date(), "timeout") + else + assert(result, err) + print(os.date(), result.path, result.action) + end +end +watcher:close() +]] +return { + new = new_watcher, +} +end --[[ Copyright 2016,2018-2019 ARATA Mizuki @@ -2143,7 +2579,7 @@ end along with ClutTeX. If not, see . ]] -CLUTTEX_VERSION = "v0.2" +CLUTTEX_VERSION = "v0.3" -- Standard libraries local coroutine = coroutine @@ -2165,6 +2601,8 @@ local message = require "texrunner.message" local extract_bibtex_from_aux_file = require "texrunner.auxfile".extract_bibtex_from_aux_file local handle_cluttex_options = require "texrunner.handleoption".handle_cluttex_options +os.setlocale("", "ctype") -- Workaround for recent Universal CRT + -- arguments: input file name, jobname, etc... local function genOutputDirectory(...) -- The name of the temporary directory is based on the path of input file. @@ -2611,6 +3049,7 @@ end if options.watch then -- Watch mode local success, status = do_typeset() + -- TODO: filenames here can be UTF-8 if command_line_encoding=utf-8 local filelist, filemap = reruncheck.parse_recorder_file(recorderfile, options) if engine.is_luatex and fsutil.isfile(recorderfile2) then filelist, filemap = reruncheck.parse_recorder_file(recorderfile2, options, filelist, filemap) @@ -2621,28 +3060,87 @@ if options.watch then table.insert(input_files_to_watch, fileinfo.abspath) end end - local fswatch_command = {"fswatch", "--event=Updated", "--"} - for _,path in ipairs(input_files_to_watch) do - table.insert(fswatch_command, shellutil.escape(path)) - end - if CLUTTEX_VERBOSITY >= 1 then - message.exec(table.concat(fswatch_command, " ")) + local fswatcherlib + if os.type == "windows" then + -- Windows: Try built-in filesystem watcher + local succ, result = pcall(require, "texrunner.fswatcher_windows") + if not succ and CLUTTEX_VERBOSITY >= 1 then + message.warn("Failed to load texrunner.fswatcher_windows: " .. result) + end + fswatcherlib = result end - local fswatch = assert(io.popen(table.concat(fswatch_command, " "), "r")) - for l in fswatch:lines() do - local found = false + if fswatcherlib then + if CLUTTEX_VERBOSITY >= 2 then + message.info("Using built-in filesystem watcher for Windows") + end + local watcher = assert(fswatcherlib.new()) for _,path in ipairs(input_files_to_watch) do - if l == path then - found = true - break - end + assert(watcher:add_file(path)) end - if found then + while true do + local result = assert(watcher:next()) + if CLUTTEX_VERBOSITY >= 2 then + message.info(string.format("%s %s"), result.action, result.path) + end local success, status = do_typeset() if not success then -- Not successful end end + elseif shellutil.has_command("fswatch") then + local fswatch_command = {"fswatch", "--event=Updated", "--"} + for _,path in ipairs(input_files_to_watch) do + table.insert(fswatch_command, shellutil.escape(path)) + end + local fswatch_command_str = table.concat(fswatch_command, " ") + if CLUTTEX_VERBOSITY >= 1 then + message.exec(fswatch_command_str) + end + local fswatch = assert(io.popen(fswatch_command_str, "r")) + for l in fswatch:lines() do + local found = false + for _,path in ipairs(input_files_to_watch) do + if l == path then + found = true + break + end + end + if found then + local success, status = do_typeset() + if not success then + -- Not successful + end + end + end + elseif shellutil.has_command("inotifywait") then + local inotifywait_command = {"inotifywait", "--monitor", "--event=modify", "--event=attrib", "--format=%w", "--quiet"} + for _,path in ipairs(input_files_to_watch) do + table.insert(inotifywait_command, shellutil.escape(path)) + end + local inotifywait_command_str = table.concat(inotifywait_command, " ") + if CLUTTEX_VERBOSITY >= 1 then + message.exec(inotifywait_command_str) + end + local inotifywait = assert(io.popen(inotifywait_command_str, "r")) + for l in inotifywait:lines() do + local found = false + for _,path in ipairs(input_files_to_watch) do + if l == path then + found = true + break + end + end + if found then + local success, status = do_typeset() + if not success then + -- Not successful + end + end + end + else + message.error("Could not watch files because neither `fswatch' nor `inotifywait' was installed.") + message.info("See ClutTeX's manual for details.") + os.exit(1) end else -- cgit v1.2.3