diff options
author | Karl Berry <karl@freefriends.org> | 2019-04-30 22:36:50 +0000 |
---|---|---|
committer | Karl Berry <karl@freefriends.org> | 2019-04-30 22:36:50 +0000 |
commit | b9d1e8848fb4f8e45796a706a0391734b0c88c7c (patch) | |
tree | 8e97fb607639f6bc1bf187b6a31cb76a4c53a5c8 | |
parent | f6aba0a218c3b19afb4ff78799e8f0760e584397 (diff) |
cluttex (30apr19)
git-svn-id: svn://tug.org/texlive/trunk@50913 c570f23f-e606-0410-a88d-b1316a301751
17 files changed, 2132 insertions, 93 deletions
diff --git a/Build/source/texk/texlive/linked_scripts/cluttex/cluttex.lua b/Build/source/texk/texlive/linked_scripts/cluttex/cluttex.lua index dc622725bee..dfbd5c5694f 100755 --- a/Build/source/texk/texlive/linked_scripts/cluttex/cluttex.lua +++ b/Build/source/texk/texlive/linked_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 <http://www.gnu.org/licenses/>. +]] + +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 <http://www.gnu.org/licenses/>. ]] -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 diff --git a/Master/texmf-dist/doc/support/cluttex/CHANGELOG.md b/Master/texmf-dist/doc/support/cluttex/CHANGELOG.md index dc1d6b20e4c..c76e6189391 100644 --- a/Master/texmf-dist/doc/support/cluttex/CHANGELOG.md +++ b/Master/texmf-dist/doc/support/cluttex/CHANGELOG.md @@ -1,3 +1,11 @@ +Version 0.3 (2019-04-30) +----- + +Changes: + +* Support other methods for watching file system: `inotifywait` for Linux and a built-in one for Windows. +* Fix `--no-*` options. + Version 0.2 (2019-02-22) ----- diff --git a/Master/texmf-dist/doc/support/cluttex/Makefile b/Master/texmf-dist/doc/support/cluttex/Makefile index 778f0d3da7e..c588900ab23 100644 --- a/Master/texmf-dist/doc/support/cluttex/Makefile +++ b/Master/texmf-dist/doc/support/cluttex/Makefile @@ -19,6 +19,7 @@ sources= \ src/texrunner/handleoption.lua \ src/texrunner/isatty.lua \ src/texrunner/message.lua \ + src/texrunner/fswatcher_windows.lua \ src/cluttex.lua bin/cluttex: $(sources) build.lua diff --git a/Master/texmf-dist/doc/support/cluttex/README.md b/Master/texmf-dist/doc/support/cluttex/README.md index 1e0f792cd9a..67354e40eed 100644 --- a/Master/texmf-dist/doc/support/cluttex/README.md +++ b/Master/texmf-dist/doc/support/cluttex/README.md @@ -62,7 +62,7 @@ Command-line Options Change the current working directory to the output directory when running TeX. * `--watch` Watch input files for change. - Requires [fswatch](http://emcrisostomo.github.io/fswatch/) program to be installed. + Requires [fswatch](http://emcrisostomo.github.io/fswatch/) program or `inotifywait` program to be installed on Unix systems. * `--color[=WHEN]` Make ClutTeX's message colorful. `WHEN` is one of `always`, `auto`, or `never`. diff --git a/Master/texmf-dist/doc/support/cluttex/bin/cluttex.bat b/Master/texmf-dist/doc/support/cluttex/bin/cluttex.bat index 2e3e8033980..7570923a7e7 100755 --- a/Master/texmf-dist/doc/support/cluttex/bin/cluttex.bat +++ b/Master/texmf-dist/doc/support/cluttex/bin/cluttex.bat @@ -347,7 +347,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. @@ -366,6 +366,7 @@ package.preload["texrunner.shellutil"] = function(...) ]] local string_gsub = string.gsub +local os_execute = os.execute -- s: string local function escape(s) @@ -373,14 +374,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. @@ -403,6 +411,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) @@ -433,8 +442,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 @@ -545,6 +561,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 @@ -584,6 +601,7 @@ local function parseoption(arg, options) elseif o.boolean and name == "no-" .. o.long then -- --no-option opt = o + param = false break end end @@ -623,6 +641,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 @@ -2127,6 +2146,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 <http://www.gnu.org/licenses/>. +]] + +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 @@ -2146,7 +2582,7 @@ end along with ClutTeX. If not, see <http://www.gnu.org/licenses/>. ]] -CLUTTEX_VERSION = "v0.2" +CLUTTEX_VERSION = "v0.3" -- Standard libraries local coroutine = coroutine @@ -2168,6 +2604,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. @@ -2614,6 +3052,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) @@ -2624,28 +3063,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 diff --git a/Master/texmf-dist/doc/support/cluttex/build.lua b/Master/texmf-dist/doc/support/cluttex/build.lua index bda6cf637a2..24d0674ffc9 100644 --- a/Master/texmf-dist/doc/support/cluttex/build.lua +++ b/Master/texmf-dist/doc/support/cluttex/build.lua @@ -28,6 +28,7 @@ elseif arg[1] == "--windows-batchfile" then table.remove(arg, 1) end local outfile = arg[1] +local preserve_location_info = false local modules = { { @@ -82,6 +83,10 @@ local modules = { name = "texrunner.message", path = "texrunner/message.lua", }, + { + name = "texrunner.fswatcher_windows", + path = "texrunner/fswatcher_windows.lua", + }, } local imported_globals = {"io", "os", "string", "table", "package", "require", "assert", "error", "ipairs", "type", "select", "arg"} @@ -102,7 +107,13 @@ local function strip_global_imports(code) end local function strip_test_code(code) - return (code:gsub("%-%- TEST CODE\n(.-)%-%- END TEST CODE\n", "")) + if preserve_location_info then + return (code:gsub("%-%- TEST CODE\n.-%-%- END TEST CODE\n", function(s) + return (s:gsub("[^\n]","")) + end)) + else + return (code:gsub("%-%- TEST CODE\n(.-)%-%- END TEST CODE\n", "")) + end end local function load_module_code(path) @@ -133,8 +144,10 @@ else end end -table.insert(lines, string.format("local %s = %s\n", table.concat(imported_globals, ", "), table.concat(imported_globals, ", "))) -table.insert(lines, "local CLUTTEX_VERBOSITY, CLUTTEX_VERSION\n") +if not preserve_location_info then + table.insert(lines, string.format("local %s = %s\n", table.concat(imported_globals, ", "), table.concat(imported_globals, ", "))) + table.insert(lines, "local CLUTTEX_VERBOSITY, CLUTTEX_VERSION\n") +end if default_os then table.insert(lines, string.format("os.type = os.type or %q\n", default_os)) @@ -142,19 +155,34 @@ end -- LuajitTeX doesn't seem to set package.loaded table... table.insert(lines, "if lfs and not package.loaded['lfs'] then package.loaded['lfs'] = lfs end\n") - -for _,m in ipairs(modules) do - if m.path_windows or m.path_unix then - table.insert(lines, 'if os.type == "windows" then\n') - table.insert(lines, string.format("package.preload[%q] = function(...)\n%send\n", m.name, load_module_code(m.path_windows or m.path))) - table.insert(lines, 'else\n') - table.insert(lines, string.format("package.preload[%q] = function(...)\n%send\n", m.name, load_module_code(m.path_unix or m.path))) - table.insert(lines, 'end\n') - else - table.insert(lines, string.format("package.preload[%q] = function(...)\n%send\n", m.name, load_module_code(m.path))) +if preserve_location_info then + table.insert(lines, "local loadstring = loadstring or load\n") + for _,m in ipairs(modules) do + if m.path_windows or m.path_unix then + table.insert(lines, 'if os.type == "windows" then\n') + table.insert(lines, string.format("package.preload[%q] = assert(loadstring(%q, %q))\n", m.name, load_module_code(m.path_windows or m.path), "=" .. (m.path_windows or m.path))) + table.insert(lines, 'else\n') + table.insert(lines, string.format("package.preload[%q] = assert(loadstring(%q, %q))\n", m.name, load_module_code(m.path_unix or m.path), "=" .. (m.path_unix or m.path))) + table.insert(lines, 'end\n') + else + table.insert(lines, string.format("package.preload[%q] = assert(loadstring(%q, %q))\n", m.name, load_module_code(m.path), "=" .. m.path)) + end + end + table.insert(lines, string.format("assert(loadstring(%q, %q))(...)\n", main, "=cluttex.lua")) +else + for _,m in ipairs(modules) do + if m.path_windows or m.path_unix then + table.insert(lines, 'if os.type == "windows" then\n') + table.insert(lines, string.format("package.preload[%q] = function(...)\n%send\n", m.name, load_module_code(m.path_windows or m.path))) + table.insert(lines, 'else\n') + table.insert(lines, string.format("package.preload[%q] = function(...)\n%send\n", m.name, load_module_code(m.path_unix or m.path))) + table.insert(lines, 'end\n') + else + table.insert(lines, string.format("package.preload[%q] = function(...)\n%send\n", m.name, load_module_code(m.path))) + end end + table.insert(lines, strip_global_imports(main)) end -table.insert(lines, strip_global_imports(main)) if outfile then io.output(assert(io.open(outfile, "wb"))) diff --git a/Master/texmf-dist/doc/support/cluttex/checkglobal.lua b/Master/texmf-dist/doc/support/cluttex/checkglobal.lua index 4b86ef9f6c4..0c765809a02 100644 --- a/Master/texmf-dist/doc/support/cluttex/checkglobal.lua +++ b/Master/texmf-dist/doc/support/cluttex/checkglobal.lua @@ -32,6 +32,7 @@ local known_global = { tostring = true, type = true, xpcall = true, + loadstring = true, -- Lua 5.1 -- Standard modules bit32 = true, -- Lua 5.2 diff --git a/Master/texmf-dist/doc/support/cluttex/doc/manual-ja.pdf b/Master/texmf-dist/doc/support/cluttex/doc/manual-ja.pdf Binary files differindex f5524674724..43292492359 100644 --- a/Master/texmf-dist/doc/support/cluttex/doc/manual-ja.pdf +++ b/Master/texmf-dist/doc/support/cluttex/doc/manual-ja.pdf diff --git a/Master/texmf-dist/doc/support/cluttex/doc/manual-ja.tex b/Master/texmf-dist/doc/support/cluttex/doc/manual-ja.tex index 9966dbb749d..cd1a47741a2 100644 --- a/Master/texmf-dist/doc/support/cluttex/doc/manual-ja.tex +++ b/Master/texmf-dist/doc/support/cluttex/doc/manual-ja.tex @@ -10,8 +10,9 @@ \renewcommand\sectionautorefname{セクション} \renewcommand\subsectionautorefname{サブセクション} -\title{\ClutTeX{}マニュアル} +\title{\ClutTeX{}マニュアル\\(バージョン0.3)} \author{ARATA Mizuki} +\date{2019年4月30日} \begin{document} \maketitle @@ -23,7 +24,7 @@ \begin{itemize} \item 作業ディレクトリを\texttt{.aux}や\texttt{.log}等の「余計な」ファイルで散らかさない \item (相互参照の解決などで)複数回処理を行う必要がある場合に、必要な回数だけ自動で処理する -\item 入力ファイルを監視し、変更があった場合に自動で再処理する(\texttt{--watch}オプション\footnote{別途プログラムが必要}) +\item 入力ファイルを監視し、変更があった場合に自動で再処理する(\texttt{--watch}オプション\footnote{Unix系OSでは、別途プログラムが必要。}) \item MakeIndex, \BibTeX, Biber等のコマンドを自動で実行する(\texttt{--makeindex}オプション, \texttt{--bibtex}オプション, \texttt{--biber}オプション) \item p\TeX 系列の処理系でPDFを生成する場合、別途\texttt{dvipdfmx}を実行する必要がない(自動で\texttt{dvipdfmx}を実行する) \end{itemize} @@ -71,7 +72,7 @@ デフォルト:3 \item[\texttt{--watch}] 入力ファイルを監視する。 - 別途、\texttt{fswatch}プログラムが必要となる。 + 別途、\texttt{fswatch}プログラムまたは\texttt{inotifywait}プログラムが必要となる場合がある。 詳しくは\autoref{sec:watch-mode}を参照。 \item[\texttt{--color[=\metavar{WHEN}]}] ターミナルへの出力を色付けする。 @@ -145,9 +146,9 @@ \section{監視モード}\label{sec:watch-mode} \ClutTeX{}に\texttt{--watch}オプションを指定して起動した場合、文書の処理後に\emph{監視モード}に入る。 -\texttt{fswatch}\footnote{\url{http://emcrisostomo.github.io/fswatch/}}プログラムが予めインストールされている必要がある。 -\ClutTeX{}の将来のバージョンでは、外部プログラムに頼らない監視モードを実装するかもしれない。 +Windows上では、\ClutTeX{}単体でファイルシステムの監視を行う。 +一方で、それ以外のOS(Unix系)では、\texttt{fswatch}\footnote{\url{http://emcrisostomo.github.io/fswatch/}}プログラムまたは\texttt{inotifywait}プログラムが予めインストールされている必要がある。 \section{MakeIndexや\BibTeX} MakeIndexや\BibTeX を使って処理を行う場合は、\texttt{--makeindex}や\texttt{--bibtex}等のオプションを指定する。 diff --git a/Master/texmf-dist/doc/support/cluttex/doc/manual.pdf b/Master/texmf-dist/doc/support/cluttex/doc/manual.pdf Binary files differindex 40e25bc9b3e..7e3e6dd6f72 100644 --- a/Master/texmf-dist/doc/support/cluttex/doc/manual.pdf +++ b/Master/texmf-dist/doc/support/cluttex/doc/manual.pdf diff --git a/Master/texmf-dist/doc/support/cluttex/doc/manual.tex b/Master/texmf-dist/doc/support/cluttex/doc/manual.tex index 4ec0464cc67..22761006cf1 100644 --- a/Master/texmf-dist/doc/support/cluttex/doc/manual.tex +++ b/Master/texmf-dist/doc/support/cluttex/doc/manual.tex @@ -8,8 +8,10 @@ \newcommand\texpkg[1]{\texttt{#1}} \newcommand\metavar[1]{\textnormal{\textsf{#1}}} -\title{\ClutTeX\ manual} +\title{\ClutTeX\ manual\\(Version 0.3)} \author{ARATA Mizuki} +\date{2019-04-30} + \begin{document} \maketitle \tableofcontents @@ -20,7 +22,7 @@ Basic features are, \begin{itemize} \item Does not clutter your working directory with ``extra'' files, like \texttt{.aux} or \texttt{.log}. \item If multiple runs are required to generate correct document, do so. -\item Watch input files, and re-process documents if changes are detected\footnote{needs an external program}. +\item Watch input files, and re-process documents if changes are detected\footnote{needs an external program if you are on a Unix system}. \item Run MakeIndex, \BibTeX, Biber, if requested. \item Produces a PDF, even if the engine (e.g.\ p\TeX) does not suport direct PDF generation. \end{itemize} @@ -65,7 +67,7 @@ Basic options: Default: 3 \item[\texttt{--watch}] Watch input files for change. - Requires \texttt{fswatch} command to be available. + May need an external program to be available. See \autoref{sec:watch-mode} for details. \item[\texttt{--color[=\metavar{WHEN}]}] Colorize messages. @@ -138,9 +140,9 @@ This is because Sync\TeX\ cannot find its data file if it's not in the same dire \section{Watch mode}\label{sec:watch-mode} If \texttt{--watch} option is given, \ClutTeX\ enters \emph{watch mode} after processing the document. -An auxiliary program \texttt{fswatch}\footnote{\url{http://emcrisostomo.github.io/fswatch/}} needs to be installed for this mode. -A future version of \ClutTeX\ may implement a built-in filesystem watcher. +On Windows, a built-in filesystem watcher is implemented. +On other platforms, an auxiliary program \texttt{fswatch}\footnote{\url{http://emcrisostomo.github.io/fswatch/}} or \texttt{inotifywait} needs to be installed. \section{MakeIndex and \BibTeX} If you want to generate index or bibliography, using MakeIndex or \BibTeX, set \texttt{--makeindex}, \texttt{--bibtex}, or \texttt{--biber} option. diff --git a/Master/texmf-dist/doc/support/cluttex/src/cluttex.lua b/Master/texmf-dist/doc/support/cluttex/src/cluttex.lua index 0e56e8bf4bd..92952af58df 100644 --- a/Master/texmf-dist/doc/support/cluttex/src/cluttex.lua +++ b/Master/texmf-dist/doc/support/cluttex/src/cluttex.lua @@ -18,7 +18,7 @@ along with ClutTeX. If not, see <http://www.gnu.org/licenses/>. ]] -CLUTTEX_VERSION = "v0.2" +CLUTTEX_VERSION = "v0.3" -- Standard libraries local table = table @@ -45,6 +45,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. @@ -491,6 +493,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) @@ -501,28 +504,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 diff --git a/Master/texmf-dist/doc/support/cluttex/src/texrunner/fswatcher_windows.lua b/Master/texmf-dist/doc/support/cluttex/src/texrunner/fswatcher_windows.lua new file mode 100644 index 00000000000..05a244093b8 --- /dev/null +++ b/Master/texmf-dist/doc/support/cluttex/src/texrunner/fswatcher_windows.lua @@ -0,0 +1,423 @@ +--[[ + 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 <http://www.gnu.org/licenses/>. +]] + +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 + +-- TEST CODE +do + local ws = {0x3042} + local resultstr = wcs_to_mbs(ffi.new("WCHAR[1]", ws), 1, CP_UTF8) + assert(#resultstr == 3) + assert(resultstr == "\xE3\x81\x82") -- \u{XXXX} notation is not available on LuaJIT +end +-- END TEST CODE + +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, +} diff --git a/Master/texmf-dist/doc/support/cluttex/src/texrunner/option.lua b/Master/texmf-dist/doc/support/cluttex/src/texrunner/option.lua index 839e3a40d67..19598610add 100644 --- a/Master/texmf-dist/doc/support/cluttex/src/texrunner/option.lua +++ b/Master/texmf-dist/doc/support/cluttex/src/texrunner/option.lua @@ -19,6 +19,7 @@ -- 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 @@ -58,6 +59,7 @@ local function parseoption(arg, options) elseif o.boolean and name == "no-" .. o.long then -- --no-option opt = o + param = false break end end @@ -97,6 +99,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 diff --git a/Master/texmf-dist/doc/support/cluttex/src/texrunner/shellutil_unix.lua b/Master/texmf-dist/doc/support/cluttex/src/texrunner/shellutil_unix.lua index c506e26ffba..70bc820e0d8 100644 --- a/Master/texmf-dist/doc/support/cluttex/src/texrunner/shellutil_unix.lua +++ b/Master/texmf-dist/doc/support/cluttex/src/texrunner/shellutil_unix.lua @@ -1,5 +1,5 @@ --[[ - Copyright 2016 ARATA Mizuki + Copyright 2016,2019 ARATA Mizuki This file is part of ClutTeX. @@ -22,6 +22,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) @@ -57,6 +58,13 @@ assert(escape([[Hello' world!]]) == [['Hello'"'"' world!']]) assert(escape([[Hello' world!"]]) == [['Hello'"'"' world!"']]) -- END TEST CODE +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, } diff --git a/Master/texmf-dist/doc/support/cluttex/src/texrunner/shellutil_windows.lua b/Master/texmf-dist/doc/support/cluttex/src/texrunner/shellutil_windows.lua index cd0cc11fe9d..a8f4d763aa0 100644 --- a/Master/texmf-dist/doc/support/cluttex/src/texrunner/shellutil_windows.lua +++ b/Master/texmf-dist/doc/support/cluttex/src/texrunner/shellutil_windows.lua @@ -1,5 +1,5 @@ --[[ - Copyright 2016 ARATA Mizuki + Copyright 2016,2019 ARATA Mizuki This file is part of ClutTeX. @@ -18,6 +18,7 @@ ]] local string_gsub = string.gsub +local os_execute = os.execute -- s: string local function escape(s) @@ -30,6 +31,13 @@ assert(escape([[Hello" world!]]) == [["Hello\" world!"]]) assert(escape([[Hello\" world!"]]) == [["Hello\\\" world!\""]]) -- END TEST CODE +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, } 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 <http://www.gnu.org/licenses/>. +]] + +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 <http://www.gnu.org/licenses/>. ]] -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 |