qdir 1.0.0rc1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- qdir/__init__.py +275 -0
- qdir/abaqus/__init__.py +5 -0
- qdir/abaqus/__main__.py +66 -0
- qdir/abaqus/qdir_plugin.py +223 -0
- qdir/abaqus/qdir_submit.py +165 -0
- qdir/qdel.py +51 -0
- qdir/qstat.py +142 -0
- qdir/qsub.py +139 -0
- qdir-1.0.0rc1.dist-info/METADATA +112 -0
- qdir-1.0.0rc1.dist-info/RECORD +14 -0
- qdir-1.0.0rc1.dist-info/WHEEL +5 -0
- qdir-1.0.0rc1.dist-info/entry_points.txt +4 -0
- qdir-1.0.0rc1.dist-info/licenses/LICENSE +21 -0
- qdir-1.0.0rc1.dist-info/top_level.txt +1 -0
qdir/__init__.py
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
#!/bin/env python3
|
|
2
|
+
"""Implementation of a first-in, first-out job queue for the local machine.
|
|
3
|
+
|
|
4
|
+
The queue is maintained as a directory of tiny job queue files.
|
|
5
|
+
When a new job is submitted, this script will identify any existing job queue files and then add its own queue file to the directory.
|
|
6
|
+
No job will begin until all the preceeding queue files are gone.
|
|
7
|
+
Each job will delete its own queue file upon completion so the next job can begin.
|
|
8
|
+
A shared queue directory should be specified on a multi-user system.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
__author__ = "Carl Osterwisch"
|
|
12
|
+
__version__ = "1.0.0rc1"
|
|
13
|
+
__url__ = "https://github.com/costerwi/qdir/"
|
|
14
|
+
|
|
15
|
+
import os
|
|
16
|
+
import signal
|
|
17
|
+
import subprocess
|
|
18
|
+
import sys
|
|
19
|
+
from argparse import ArgumentParser, Namespace
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from tempfile import gettempdir
|
|
22
|
+
from time import asctime, perf_counter, sleep
|
|
23
|
+
|
|
24
|
+
from filelock import FileLock
|
|
25
|
+
|
|
26
|
+
default = Namespace
|
|
27
|
+
|
|
28
|
+
parser = ArgumentParser(epilog=f"qdir {__version__} {__url__}")
|
|
29
|
+
default.queue = Path(os.environ.get("QDIR_QUEUE", gettempdir()))
|
|
30
|
+
parser.add_argument(
|
|
31
|
+
"-q",
|
|
32
|
+
"--queue",
|
|
33
|
+
type=Path,
|
|
34
|
+
help="Directory to store the queue files. "
|
|
35
|
+
f"Default is environment variable QDIR_QUEUE or {default.queue}",
|
|
36
|
+
default=default.queue,
|
|
37
|
+
)
|
|
38
|
+
default.verbose = int(os.environ.get("QDIR_VERBOSE", "2"))
|
|
39
|
+
parser.add_argument(
|
|
40
|
+
"-v",
|
|
41
|
+
"--verbose",
|
|
42
|
+
type=int,
|
|
43
|
+
help="Verbosity level. "
|
|
44
|
+
f"Default is environment variable QDIR_VERBOSE or {default.verbose}",
|
|
45
|
+
default=default.verbose,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
qFileSuffix = os.environ.get("QDIR_SUFFIX", ".qdir") # suffix for queue files
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class Queue:
|
|
52
|
+
def __init__(self, **kwargs):
|
|
53
|
+
"""Create the queue, allowing parameters to override defaults"""
|
|
54
|
+
for parameter, value in vars(default).items():
|
|
55
|
+
setattr(self, parameter, kwargs.pop(parameter, value))
|
|
56
|
+
if not self.queue.is_dir():
|
|
57
|
+
raise FileNotFoundError(f"The QDIR directory {self.queue} does not exist")
|
|
58
|
+
if not os.access(self.queue, os.W_OK):
|
|
59
|
+
raise PermissionError(f"The QDIR directory {self.queue} is not writable")
|
|
60
|
+
|
|
61
|
+
def getQueuedJobs(self) -> list[Path]:
|
|
62
|
+
"""Return a list of qFilePath for currently queued jobs, sorted oldest to newest."""
|
|
63
|
+
# Collecct a set of active process ids
|
|
64
|
+
if os.name == "nt":
|
|
65
|
+
import win32process
|
|
66
|
+
|
|
67
|
+
processIds = {str(pid) for pid in win32process.EnumProcesses()}
|
|
68
|
+
else:
|
|
69
|
+
procPath = Path("/proc")
|
|
70
|
+
processIds = {p.stem for p in procPath.iterdir() if p.stem.isnumeric()}
|
|
71
|
+
|
|
72
|
+
def is_qFile(path):
|
|
73
|
+
"""Verify the path is an active job file with a matching process id"""
|
|
74
|
+
if not path.stem.isnumeric():
|
|
75
|
+
return False # file name must be an integer
|
|
76
|
+
if not (path.is_file()):
|
|
77
|
+
return False # path must represent a file
|
|
78
|
+
if path.stem in processIds:
|
|
79
|
+
return True # file name must match a running process number
|
|
80
|
+
|
|
81
|
+
# Otherwise, the process may have died or the computer was rebooted with
|
|
82
|
+
# this job in the queue. This orphaned qFile may be removed.
|
|
83
|
+
print(
|
|
84
|
+
"Removing orphaned queue file",
|
|
85
|
+
self.info.format(**self.getJobInfo(path)),
|
|
86
|
+
file=sys.stderr,
|
|
87
|
+
)
|
|
88
|
+
try:
|
|
89
|
+
path.unlink(missing_ok=True)
|
|
90
|
+
except OSError as e:
|
|
91
|
+
print(e, file=sys.stderr)
|
|
92
|
+
|
|
93
|
+
queuedJobs = [
|
|
94
|
+
filePath
|
|
95
|
+
for filePath in self.queue.glob("*" + qFileSuffix)
|
|
96
|
+
if is_qFile(filePath)
|
|
97
|
+
]
|
|
98
|
+
return sorted(queuedJobs, key=lambda qFilePath: qFilePath.stat().st_mtime)
|
|
99
|
+
|
|
100
|
+
def getJobInfo(self, qFilePath: Path) -> dict:
|
|
101
|
+
"""Return information about the job specified in qFilePath"""
|
|
102
|
+
jobInfo = {
|
|
103
|
+
"jobId": qFilePath.stem,
|
|
104
|
+
"owner": qFilePath.owner(),
|
|
105
|
+
"name": "",
|
|
106
|
+
"command": "",
|
|
107
|
+
"directory": "",
|
|
108
|
+
"behind": "",
|
|
109
|
+
"status": "",
|
|
110
|
+
"history": [], # list[str]
|
|
111
|
+
}
|
|
112
|
+
try:
|
|
113
|
+
with qFilePath.open() as qFile:
|
|
114
|
+
for line in qFile:
|
|
115
|
+
keyValue = line.strip().split("\t", 2)
|
|
116
|
+
if len(keyValue) == 2:
|
|
117
|
+
jobInfo.update([keyValue])
|
|
118
|
+
else:
|
|
119
|
+
status = keyValue[0]
|
|
120
|
+
jobInfo["history"].append(status)
|
|
121
|
+
jobInfo["status"] = status # latest update
|
|
122
|
+
except OSError as e:
|
|
123
|
+
status = repr(e)
|
|
124
|
+
jobInfo["history"].append(status)
|
|
125
|
+
jobInfo["status"] = status
|
|
126
|
+
return jobInfo
|
|
127
|
+
|
|
128
|
+
def dequeue(self, qFilePath: Path):
|
|
129
|
+
"""Remove a job from the queue"""
|
|
130
|
+
assert qFilePath.parent == self.queue
|
|
131
|
+
assert qFilePath.suffix == qFileSuffix
|
|
132
|
+
assert qFilePath.stem.isnumeric()
|
|
133
|
+
# Removing qFile will dequeue the job but the effect is delayed by polling interval
|
|
134
|
+
qFilePath.unlink(missing_ok=True)
|
|
135
|
+
if os.name == "posix":
|
|
136
|
+
# On Linux the SIGTERM signal has immediate effect
|
|
137
|
+
try:
|
|
138
|
+
os.kill(int(qFilePath.stem), signal.SIGTERM)
|
|
139
|
+
except OSError: # no such process
|
|
140
|
+
pass
|
|
141
|
+
|
|
142
|
+
def enqueue(self, cmd: list[str]):
|
|
143
|
+
"""Wait in queue until prior jobs finish, then run cmd"""
|
|
144
|
+
import getpass
|
|
145
|
+
import shutil
|
|
146
|
+
|
|
147
|
+
if len(cmd) < 1:
|
|
148
|
+
raise SyntaxError("Missing cmd")
|
|
149
|
+
if self.verbose >= 1 and not shutil.which(cmd[0]):
|
|
150
|
+
print(f"WARNING: {cmd[0]} is not in PATH", file=sys.stderr)
|
|
151
|
+
if not self.interval > 0:
|
|
152
|
+
self.interval = 1
|
|
153
|
+
|
|
154
|
+
startTime = perf_counter() # time job was enqueued
|
|
155
|
+
job = None # subprocess running the cmd, if any
|
|
156
|
+
|
|
157
|
+
jobInfo = { # information about this job
|
|
158
|
+
"jobId": str(os.getpid()), # process id of this script
|
|
159
|
+
"owner": getpass.getuser(),
|
|
160
|
+
"name": self.name or cmd[-1],
|
|
161
|
+
"command": " ".join(cmd),
|
|
162
|
+
"directory": Path.cwd(),
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
def updateStatus(priority, status, file=None):
|
|
166
|
+
"""Notify user of status change and optionally record in file"""
|
|
167
|
+
jobInfo["status"] = status
|
|
168
|
+
jobInfo["time"] = asctime()
|
|
169
|
+
jobInfo["duration"] = humanDuration(perf_counter() - startTime)
|
|
170
|
+
if self.verbose >= priority:
|
|
171
|
+
# Echo status to the terminal
|
|
172
|
+
print(self.status.format(**jobInfo))
|
|
173
|
+
if file is not None:
|
|
174
|
+
# Record status history in specified qFile
|
|
175
|
+
print("{time} {status}".format(**jobInfo), file=file)
|
|
176
|
+
|
|
177
|
+
qFilePath = (self.queue / jobInfo["jobId"]).with_suffix(qFileSuffix)
|
|
178
|
+
lock = FileLock(self.queue / "qdir.lock") # prevent previousJobs race
|
|
179
|
+
with lock.acquire(timeout=5):
|
|
180
|
+
previousJobs = self.getQueuedJobs()
|
|
181
|
+
jobInfo["behind"] = " ".join([job.stem for job in previousJobs])
|
|
182
|
+
with qFilePath.open("w") as qFile:
|
|
183
|
+
os.environ["QDIR_FILE"] = qFile.name # will be exported to cmd
|
|
184
|
+
# Initialize qFile contents
|
|
185
|
+
for key in "name", "command", "directory", "behind":
|
|
186
|
+
value = jobInfo.get(key)
|
|
187
|
+
if value:
|
|
188
|
+
print(key, value, sep="\t", file=qFile)
|
|
189
|
+
updateStatus(priority=2, status="Queued", file=qFile)
|
|
190
|
+
|
|
191
|
+
def terminateJob(sig=None, frame=None):
|
|
192
|
+
"""Terminate this job if running or dequeue if not"""
|
|
193
|
+
nonlocal job
|
|
194
|
+
result = ""
|
|
195
|
+
qFilePath.unlink(missing_ok=True) # remove its qFile
|
|
196
|
+
if job is None:
|
|
197
|
+
# removed from the queue before starting
|
|
198
|
+
result = ".. Dequeued"
|
|
199
|
+
elif hasattr(job, "pid"): # job is or was running
|
|
200
|
+
if job.poll() is None:
|
|
201
|
+
# job is still running
|
|
202
|
+
if self.verbose >= 2:
|
|
203
|
+
result = ".. Killed"
|
|
204
|
+
if os.name == "nt":
|
|
205
|
+
job.send_signal(signal.CTRL_BREAK_EVENT)
|
|
206
|
+
else:
|
|
207
|
+
# pass SIGTERM signal to all child processes in this process group
|
|
208
|
+
os.killpg(os.getpgid(job.pid), signal.SIGTERM)
|
|
209
|
+
elif self.verbose >= 1:
|
|
210
|
+
# job is no longer running; check returncode
|
|
211
|
+
if job.returncode <= 0:
|
|
212
|
+
result = "** Done"
|
|
213
|
+
else:
|
|
214
|
+
result = "-- Error"
|
|
215
|
+
if result:
|
|
216
|
+
updateStatus(priority=2, status=result)
|
|
217
|
+
job = result # prevent this from duplicate reporting
|
|
218
|
+
|
|
219
|
+
signal.signal(
|
|
220
|
+
signal.SIGTERM, terminateJob
|
|
221
|
+
) # catch termination signal (ignored on Windows)
|
|
222
|
+
|
|
223
|
+
# Wait for all previousJobs to complete
|
|
224
|
+
previousJobs.reverse() # check newest jobs first
|
|
225
|
+
while qFilePath.exists() and any(
|
|
226
|
+
otherFile.exists() for otherFile in previousJobs
|
|
227
|
+
):
|
|
228
|
+
sleep(self.interval)
|
|
229
|
+
|
|
230
|
+
# Launch the cmd only if this job's qFile still exists
|
|
231
|
+
if qFilePath.exists():
|
|
232
|
+
try:
|
|
233
|
+
# Append the job starting status to qFile
|
|
234
|
+
stat = qFilePath.stat()
|
|
235
|
+
with qFilePath.open("a") as qFile:
|
|
236
|
+
print(
|
|
237
|
+
"Waited in queue",
|
|
238
|
+
humanDuration(perf_counter() - startTime),
|
|
239
|
+
file=qFile,
|
|
240
|
+
)
|
|
241
|
+
updateStatus(priority=3, status="Started", file=qFile)
|
|
242
|
+
# Keep mtime unchanged so the job files sort correctly
|
|
243
|
+
os.utime(qFilePath, (stat.st_atime, stat.st_mtime))
|
|
244
|
+
except OSError:
|
|
245
|
+
pass # ignore status update exceptions
|
|
246
|
+
logFilePath = Path(self.output.format(**jobInfo))
|
|
247
|
+
with logFilePath.open("w") as logFile:
|
|
248
|
+
opts = {}
|
|
249
|
+
if os.name == "nt":
|
|
250
|
+
opts["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP
|
|
251
|
+
try:
|
|
252
|
+
job = subprocess.Popen(
|
|
253
|
+
cmd,
|
|
254
|
+
shell=False,
|
|
255
|
+
start_new_session=True,
|
|
256
|
+
stdin=subprocess.DEVNULL,
|
|
257
|
+
stdout=logFile,
|
|
258
|
+
stderr=subprocess.STDOUT,
|
|
259
|
+
**opts,
|
|
260
|
+
)
|
|
261
|
+
startTime = perf_counter() # reset to time job started running
|
|
262
|
+
while qFilePath.exists() and job.poll() is None:
|
|
263
|
+
# job is still running
|
|
264
|
+
sleep(self.interval)
|
|
265
|
+
except OSError as e:
|
|
266
|
+
print(e, file=logFile)
|
|
267
|
+
|
|
268
|
+
terminateJob() # Clean up at the end
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def humanDuration(seconds: float) -> str:
|
|
272
|
+
for unit, per in ("days", 86400), ("hours", 3600), ("minutes", 60):
|
|
273
|
+
if seconds >= 1.1 * per:
|
|
274
|
+
return f"{seconds / per:.2g} {unit}"
|
|
275
|
+
return f"{seconds:.2g} seconds"
|
qdir/abaqus/__init__.py
ADDED
qdir/abaqus/__main__.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Install or uninstall qdir for Abaqus.
|
|
2
|
+
|
|
3
|
+
This script will modify the user's abaqus_v6.env file with
|
|
4
|
+
settings for the qdir job queue.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import print_function, with_statement # use Python3 syntax
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
from argparse import ArgumentParser
|
|
12
|
+
|
|
13
|
+
settings = """# Settings for qdir job queue
|
|
14
|
+
sys.path.append(r"{}") # directory of qdir_submit.py
|
|
15
|
+
import qdir_submit
|
|
16
|
+
qdir_submit.init(queues)
|
|
17
|
+
plugin_central_dir += os.pathsep + qdir_submit.dirname
|
|
18
|
+
run_mode = BATCH # command line will submit to queue by default"""
|
|
19
|
+
|
|
20
|
+
parser = ArgumentParser(description=__doc__)
|
|
21
|
+
parser.add_argument(
|
|
22
|
+
"action",
|
|
23
|
+
choices=["install", "uninstall"],
|
|
24
|
+
help="Specify what to do.",
|
|
25
|
+
)
|
|
26
|
+
default = os.path.expanduser("~/abaqus_v6.env")
|
|
27
|
+
parser.add_argument(
|
|
28
|
+
"env",
|
|
29
|
+
nargs="?",
|
|
30
|
+
default=default,
|
|
31
|
+
help="Optionally specify the .env file to modify. Default is {}".format(default),
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def uninstall(user_env):
|
|
36
|
+
"""Remove qdir settings from user's abaqus_v6.env"""
|
|
37
|
+
new_env = user_env + ".new"
|
|
38
|
+
with open(new_env, "w") as new_env_file:
|
|
39
|
+
try:
|
|
40
|
+
# copy current settings
|
|
41
|
+
with open(user_env) as env_file:
|
|
42
|
+
for line in env_file:
|
|
43
|
+
if not "qdir" in line: # skip any qdir settings
|
|
44
|
+
print(line.rstrip(), file=new_env_file)
|
|
45
|
+
os.remove(user_env)
|
|
46
|
+
except FileNotFoundError:
|
|
47
|
+
pass
|
|
48
|
+
os.rename(new_env, user_env)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def install(user_env):
|
|
52
|
+
"""Append qdir settings to user's abaqus_v6.env"""
|
|
53
|
+
uninstall(user_env) # remove any previous settings
|
|
54
|
+
dirname = os.path.dirname(os.path.normpath(__file__))
|
|
55
|
+
with open(user_env, "a") as user_env_file:
|
|
56
|
+
print(settings.format(dirname), file=user_env_file)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
if __name__ == "__main__":
|
|
60
|
+
options = parser.parse_args(sys.argv[1:])
|
|
61
|
+
if "uninstall" == options.action:
|
|
62
|
+
print("Removing qdir settings from", options.env)
|
|
63
|
+
uninstall(options.env)
|
|
64
|
+
else:
|
|
65
|
+
print("Adding qdir settings to", options.env)
|
|
66
|
+
install(options.env)
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"""Abaqus CAE plugin to interact with qdir Queue"""
|
|
2
|
+
|
|
3
|
+
from __future__ import print_function, with_statement
|
|
4
|
+
|
|
5
|
+
__version__ = "1.0.0"
|
|
6
|
+
__author__ = "Carl Osterwisch"
|
|
7
|
+
__url__ = "https://github.com/costerwi/qdir/"
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
from subprocess import PIPE, Popen
|
|
12
|
+
|
|
13
|
+
from abaqusGui import *
|
|
14
|
+
|
|
15
|
+
if isinstance("unicode", str):
|
|
16
|
+
|
|
17
|
+
def a(unicodeString):
|
|
18
|
+
"Do nothing for Python3, Abaqus >=2024"
|
|
19
|
+
return unicodeString
|
|
20
|
+
else:
|
|
21
|
+
|
|
22
|
+
def a(unicodeString):
|
|
23
|
+
"Convert unicode to str for Abaqus <2024"
|
|
24
|
+
return unicodeString.encode("ascii", "ignore")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class QDirQueueMonitor(AFXDataDialog):
|
|
28
|
+
(ID_TABLE, ID_REFRESH, ID_HELP, ID_SUBMITALL, ID_LAST) = range(
|
|
29
|
+
AFXDataDialog.ID_LAST, AFXDataDialog.ID_LAST + 5
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
def __init__(self, form):
|
|
33
|
+
AFXDataDialog.__init__(
|
|
34
|
+
self,
|
|
35
|
+
form,
|
|
36
|
+
title="QDir Job Queue Monitor",
|
|
37
|
+
opts=DIALOG_NORMAL | DECOR_RESIZE,
|
|
38
|
+
)
|
|
39
|
+
self.appendActionButton("Queue all Jobs", self, self.ID_SUBMITALL)
|
|
40
|
+
FXMAPFUNC(self, SEL_COMMAND, self.ID_SUBMITALL, QDirQueueMonitor.submitAll)
|
|
41
|
+
self.appendActionButton("Help", self, self.ID_HELP)
|
|
42
|
+
FXMAPFUNC(self, SEL_COMMAND, self.ID_HELP, QDirQueueMonitor.getHelp)
|
|
43
|
+
self.appendActionButton(self.DISMISS)
|
|
44
|
+
self.updateTimer = None
|
|
45
|
+
self.headings = ("jobId", "status", "name")
|
|
46
|
+
self.jobData = {}
|
|
47
|
+
|
|
48
|
+
sp = FXSplitter(self, LAYOUT_FILL_X | LAYOUT_FILL_Y | SPLITTER_VERTICAL)
|
|
49
|
+
self.table = AFXTable(
|
|
50
|
+
p=sp,
|
|
51
|
+
numVisRows=6,
|
|
52
|
+
numVisColumns=len(self.headings),
|
|
53
|
+
numRows=1,
|
|
54
|
+
numColumns=len(self.headings),
|
|
55
|
+
tgt=self,
|
|
56
|
+
sel=self.ID_TABLE,
|
|
57
|
+
opts=AFXTABLE_NORMAL | AFXTABLE_ROW_MODE,
|
|
58
|
+
)
|
|
59
|
+
FXMAPFUNC(self, SEL_SELECTED, self.ID_TABLE, QDirQueueMonitor.onSelected)
|
|
60
|
+
FXMAPFUNC(self, SEL_DELETED, self.ID_TABLE, QDirQueueMonitor.onDelete)
|
|
61
|
+
self.table.setLeadingRows(numRows=1)
|
|
62
|
+
self.table.setLeadingRowLabels("\t".join([h.title() for h in self.headings]))
|
|
63
|
+
self.table.setColumnWidth(column=0, width=0) # Hide the jobIds
|
|
64
|
+
self.table.setColumnWidthInChars(column=1, numChars=30)
|
|
65
|
+
self.table.setStretchableColumn(
|
|
66
|
+
len(self.headings) - 1
|
|
67
|
+
) # Expand name as necessary
|
|
68
|
+
self.table.setPopupOptions(AFXTable.POPUP_DELETE_ROW)
|
|
69
|
+
self.jobDetails = FXText(p=sp, opts=TEXT_READONLY | LAYOUT_FILL_Y)
|
|
70
|
+
self.jobDetails.setVisRows(5)
|
|
71
|
+
try:
|
|
72
|
+
self.interval = max(1, int(os.environ.get("QDIR_POLL_INTERVAL", "5")))
|
|
73
|
+
except ValueError:
|
|
74
|
+
self.interval = 5
|
|
75
|
+
FXMAPFUNC(self, SEL_TIMEOUT, self.ID_REFRESH, QDirQueueMonitor.updateTable)
|
|
76
|
+
|
|
77
|
+
def qstat(self):
|
|
78
|
+
"""Collect jobInfo of currently queued jobs"""
|
|
79
|
+
self.jobData.clear()
|
|
80
|
+
cmd = ["qstat", "--json"]
|
|
81
|
+
env = os.environ.copy()
|
|
82
|
+
env["PYTHONPATH"] = "" # remove Abaqus modified PYTHONPATH
|
|
83
|
+
try:
|
|
84
|
+
process = Popen(cmd, stdout=PIPE, stderr=PIPE, env=env)
|
|
85
|
+
stdout, stderr = process.communicate()
|
|
86
|
+
returncode = process.returncode
|
|
87
|
+
except OSError as e:
|
|
88
|
+
returncode = "OSError"
|
|
89
|
+
stdout = ""
|
|
90
|
+
stderr = str(e)
|
|
91
|
+
if returncode:
|
|
92
|
+
# report error data
|
|
93
|
+
jobInfo = {
|
|
94
|
+
"status": "Error {}".format(returncode),
|
|
95
|
+
"name": " ".join(cmd),
|
|
96
|
+
"stdout": stdout,
|
|
97
|
+
"stderr": stderr,
|
|
98
|
+
}
|
|
99
|
+
self.jobData["1"] = "1", jobInfo
|
|
100
|
+
else:
|
|
101
|
+
queuedJobs = json.loads(stdout)
|
|
102
|
+
for qFilePath, jobInfo in queuedJobs.items():
|
|
103
|
+
self.jobData[jobInfo["jobId"]] = qFilePath, jobInfo
|
|
104
|
+
|
|
105
|
+
def detailedText(self, jobInfo):
|
|
106
|
+
"""Summarize full job status"""
|
|
107
|
+
h = []
|
|
108
|
+
for key, value in jobInfo.items():
|
|
109
|
+
if "status" == key:
|
|
110
|
+
continue
|
|
111
|
+
if not value:
|
|
112
|
+
continue
|
|
113
|
+
if isinstance(value, list):
|
|
114
|
+
h.append(key.upper())
|
|
115
|
+
h.extend(value)
|
|
116
|
+
else:
|
|
117
|
+
h.append("{}\t{}".format(key.upper(), value))
|
|
118
|
+
return "\n".join(h)
|
|
119
|
+
|
|
120
|
+
def updateTable(self, *args):
|
|
121
|
+
"""Update manager with list of currently queued jobs"""
|
|
122
|
+
row = self.table.getCurrentRow()
|
|
123
|
+
if row < 1:
|
|
124
|
+
selected = None # nothing is selected
|
|
125
|
+
else:
|
|
126
|
+
selected = self.table.getItemText(row, column=0)
|
|
127
|
+
self.qstat()
|
|
128
|
+
diff = len(self.jobData) + 1 - self.table.getNumRows()
|
|
129
|
+
if diff < 0:
|
|
130
|
+
self.table.deleteRows(
|
|
131
|
+
startRow=1,
|
|
132
|
+
numRows=-diff,
|
|
133
|
+
notify=FALSE, # delete from top
|
|
134
|
+
)
|
|
135
|
+
elif diff > 0:
|
|
136
|
+
self.table.insertRows(
|
|
137
|
+
startRow=self.table.getNumRows(), # add to bottom
|
|
138
|
+
numRows=diff,
|
|
139
|
+
notify=FALSE,
|
|
140
|
+
)
|
|
141
|
+
jobDetails = ""
|
|
142
|
+
for row, (qFilePath, jobInfo) in enumerate(self.jobData.values(), 1):
|
|
143
|
+
for column, heading in enumerate(self.headings):
|
|
144
|
+
self.table.setItemValue(
|
|
145
|
+
row=row,
|
|
146
|
+
column=column,
|
|
147
|
+
valueText=a(jobInfo.get(heading, "")),
|
|
148
|
+
)
|
|
149
|
+
if jobInfo["jobId"] == selected or (selected is None and row == 1):
|
|
150
|
+
jobDetails = self.detailedText(jobInfo)
|
|
151
|
+
self.jobDetails.setText(a(jobDetails))
|
|
152
|
+
self.updateTimer = getAFXApp().addTimeout(
|
|
153
|
+
1000 * self.interval, self, self.ID_REFRESH
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
def onSelected(self, sender, sel, ptr):
|
|
157
|
+
"""Show the details for selected job"""
|
|
158
|
+
row = sender.getCurrentRow()
|
|
159
|
+
if row > 0:
|
|
160
|
+
selected = self.table.getItemText(row, column=0)
|
|
161
|
+
_qFilePath, jobInfo = self.jobData.get(selected, ("", {}))
|
|
162
|
+
jobDetails = self.detailedText(jobInfo)
|
|
163
|
+
self.jobDetails.setText(a(jobDetails))
|
|
164
|
+
|
|
165
|
+
def onDelete(self, sender, sel, ptr):
|
|
166
|
+
"""Dequeue jobs for selected table rows"""
|
|
167
|
+
cmd = ["qdel"]
|
|
168
|
+
cmd.extend(
|
|
169
|
+
self.table.getItemText(row, column=0)
|
|
170
|
+
for row in range(1, self.table.getNumRows())
|
|
171
|
+
if self.table.isRowSelected(row)
|
|
172
|
+
)
|
|
173
|
+
if len(cmd) > 1:
|
|
174
|
+
self.jobDetails.setText("")
|
|
175
|
+
env = os.environ.copy()
|
|
176
|
+
env["PYTHONPATH"] = "" # remove Abaqus modified PYTHONPATH
|
|
177
|
+
process = Popen(cmd, env=env)
|
|
178
|
+
process.wait()
|
|
179
|
+
|
|
180
|
+
def submitAll(self, sender, sel, ptr):
|
|
181
|
+
"""Submit all CAE jobs"""
|
|
182
|
+
sendCommand("qdir_submit.submitCAEJobs()")
|
|
183
|
+
return 1
|
|
184
|
+
|
|
185
|
+
def getHelp(self, sender, sel, ptr):
|
|
186
|
+
"""Open webbrowser to plugin helpUrl"""
|
|
187
|
+
import webbrowser
|
|
188
|
+
|
|
189
|
+
webbrowser.open(__url__)
|
|
190
|
+
return 1
|
|
191
|
+
|
|
192
|
+
def show(self):
|
|
193
|
+
"""Update the job data"""
|
|
194
|
+
AFXDataDialog.show(self)
|
|
195
|
+
self.updateTable() # update the table and start the updateTimer
|
|
196
|
+
|
|
197
|
+
def hide(self):
|
|
198
|
+
"""Remove the updateTimer"""
|
|
199
|
+
if self.updateTimer is not None:
|
|
200
|
+
getAFXApp().removeTimeout(self.updateTimer)
|
|
201
|
+
self.updateTimer = None
|
|
202
|
+
AFXDataDialog.hide(self)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
class QDirForm(AFXForm):
|
|
206
|
+
def getFirstDialog(self):
|
|
207
|
+
return QDirQueueMonitor(self)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
###########################################################################
|
|
211
|
+
# Register the plugin
|
|
212
|
+
###########################################################################
|
|
213
|
+
toolset = getAFXApp().getAFXMainWindow().getPluginToolset()
|
|
214
|
+
|
|
215
|
+
toolset.registerGuiMenuButton(
|
|
216
|
+
buttonText="&QDir Job Queue Monitor...",
|
|
217
|
+
object=QDirForm(toolset),
|
|
218
|
+
kernelInitString="import qdir_submit # {}".format(__version__),
|
|
219
|
+
author=__author__,
|
|
220
|
+
version=str(__version__),
|
|
221
|
+
helpUrl=__url__,
|
|
222
|
+
description="Check status of jobs in the QDir Queue",
|
|
223
|
+
)
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""Abaqus Queue class to create a job script and submit it to qdir
|
|
2
|
+
|
|
3
|
+
This module is compatible with at least Abaqus 2019 thru 2026.
|
|
4
|
+
This module works under Windows and Linux.
|
|
5
|
+
|
|
6
|
+
Carl Osterwisch, January 2024
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import print_function, with_statement # use Python3 syntax
|
|
10
|
+
|
|
11
|
+
__all__ = ["QDirQueue"]
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
from string import Template
|
|
15
|
+
from textwrap import dedent
|
|
16
|
+
|
|
17
|
+
import uti
|
|
18
|
+
from driverQueues import Queue
|
|
19
|
+
|
|
20
|
+
dirname = os.path.dirname(os.path.normpath(__file__))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def init(queues, queue=None):
|
|
24
|
+
"""Add the queue instance, QDirQueue() by default, to dict of solver queues"""
|
|
25
|
+
if queue is None:
|
|
26
|
+
queue = QDirQueue()
|
|
27
|
+
qName = getattr(queue, "name", "qdir")
|
|
28
|
+
queues[qName] = queue # add to solver
|
|
29
|
+
queues.setdefault("default", queue)
|
|
30
|
+
initCAE(qName) # also add to CAE
|
|
31
|
+
return qName
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def initCAE(qName):
|
|
35
|
+
"""Register callback to add qName to Abaqus CAE Job queues"""
|
|
36
|
+
try:
|
|
37
|
+
from abaqus import addImportCallback, session
|
|
38
|
+
except ImportError:
|
|
39
|
+
return # not running within CAE
|
|
40
|
+
|
|
41
|
+
def addQueue(*args):
|
|
42
|
+
"""Add qName to list of selectable Abaqus CAE Job queues"""
|
|
43
|
+
if qName not in session.queues:
|
|
44
|
+
session.Queue(name=qName, queueName=qName)
|
|
45
|
+
|
|
46
|
+
addImportCallback("job", addQueue)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def submitCAEJobs(jobList=None, qName="qdir"):
|
|
50
|
+
"""For specified CAE Jobs (all by default), set the queue and submit to queue"""
|
|
51
|
+
from abaqus import mdb
|
|
52
|
+
|
|
53
|
+
for job in mdb.jobs.values():
|
|
54
|
+
if not jobList or job.name in jobList:
|
|
55
|
+
print("Submitting {} to {}".format(job.name, qName))
|
|
56
|
+
job.setValues(queue=qName)
|
|
57
|
+
job.submit()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class QDirQueue(Queue):
|
|
61
|
+
"""Creates a job script and submits it to a qdir queue."""
|
|
62
|
+
|
|
63
|
+
def __init__(self):
|
|
64
|
+
if hasattr(Queue, "__init__"):
|
|
65
|
+
Queue.__init__(self)
|
|
66
|
+
self.name = "qdir"
|
|
67
|
+
if uti.getPlatform().startswith("win"):
|
|
68
|
+
# Windows OS
|
|
69
|
+
self.extension = ".cmd" # job script filename extension
|
|
70
|
+
self.setenv = "set" # shell command to set environment variables
|
|
71
|
+
self.template = Template( # job script template
|
|
72
|
+
dedent(
|
|
73
|
+
"""\
|
|
74
|
+
@echo off
|
|
75
|
+
rem QDIR --name "$job"
|
|
76
|
+
rem QDIR --output "{name}.log"
|
|
77
|
+
set JOB="$job"
|
|
78
|
+
set ABAQUS="$abaqus"
|
|
79
|
+
$exports
|
|
80
|
+
cd /D "%~dp0"
|
|
81
|
+
|
|
82
|
+
set t=0
|
|
83
|
+
:do
|
|
84
|
+
if not exist %JOB%.lck goto done
|
|
85
|
+
set /a t+=5
|
|
86
|
+
echo Paused %t% seconds for %JOB%.lck to clear
|
|
87
|
+
ping 127.0.0.1 -n 6 >nul
|
|
88
|
+
if not %t%==90 goto do
|
|
89
|
+
:done
|
|
90
|
+
|
|
91
|
+
%ABAQUS% python -u %JOB%.com
|
|
92
|
+
"""
|
|
93
|
+
)
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
else:
|
|
97
|
+
# Non-Windows OS
|
|
98
|
+
self.extension = ".sh" # job script filename extension
|
|
99
|
+
self.setenv = "export" # shell command to set environment variables
|
|
100
|
+
self.template = Template( # job script template
|
|
101
|
+
dedent(
|
|
102
|
+
"""\
|
|
103
|
+
#!/bin/sh
|
|
104
|
+
#QDIR --name "$job"
|
|
105
|
+
#QDIR --output "{name}.log"
|
|
106
|
+
JOB="$job"
|
|
107
|
+
ABAQUS="$abaqus"
|
|
108
|
+
$exports
|
|
109
|
+
cd "$$(dirname "$$0")"
|
|
110
|
+
|
|
111
|
+
let t=0
|
|
112
|
+
while [[ -e "$$JOB.lck" && $$t != 90 ]]
|
|
113
|
+
do
|
|
114
|
+
let t+=5
|
|
115
|
+
echo Paused $$t seconds for $$JOB.lck to clear
|
|
116
|
+
sleep 5
|
|
117
|
+
done
|
|
118
|
+
|
|
119
|
+
umask 0007
|
|
120
|
+
exec "$$ABAQUS" python -u "$$JOB.com"
|
|
121
|
+
"""
|
|
122
|
+
)
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
def exports(self, options, env):
|
|
126
|
+
"""Return a dict of os.environ settings to be inserted into the command script"""
|
|
127
|
+
extraEnviron = {}
|
|
128
|
+
if uti.getProductVersion() == "Abaqus 2026":
|
|
129
|
+
extraEnviron["ABQ_DLALLOCATOR"] = "1" # example usage for BR10000415659
|
|
130
|
+
return extraEnviron
|
|
131
|
+
|
|
132
|
+
def createJobScript(self, options, env):
|
|
133
|
+
"""Create shell script to run Abaqus job"""
|
|
134
|
+
job = options["job"]
|
|
135
|
+
scriptName = os.path.join(".", job + self.extension)
|
|
136
|
+
exports = self.exports(options, env) or ""
|
|
137
|
+
if isinstance(exports, dict):
|
|
138
|
+
# expand dict of os.environ settings to multiline str
|
|
139
|
+
exports = "\n".join(
|
|
140
|
+
self.setenv + " {0}={1}".format(*i) for i in sorted(exports.items())
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
with open(scriptName, "w") as jobFile:
|
|
144
|
+
print(
|
|
145
|
+
self.template.substitute(
|
|
146
|
+
abaqus=env["ABA_COMMAND"],
|
|
147
|
+
job=job,
|
|
148
|
+
exports=exports,
|
|
149
|
+
),
|
|
150
|
+
file=jobFile,
|
|
151
|
+
)
|
|
152
|
+
try:
|
|
153
|
+
os.chmod(scriptName, 0o775) # Set permissions
|
|
154
|
+
except OSError:
|
|
155
|
+
pass
|
|
156
|
+
return scriptName
|
|
157
|
+
|
|
158
|
+
def submit(self, options, env):
|
|
159
|
+
"""Create Abaqus job script and submit it to qdir"""
|
|
160
|
+
scriptName = self.createJobScript(options, env)
|
|
161
|
+
cmd = "qsub {!r}".format(scriptName)
|
|
162
|
+
localenv = env.copy()
|
|
163
|
+
if "PYTHONPATH" in localenv:
|
|
164
|
+
del localenv["PYTHONPATH"] # avoid version conflicts
|
|
165
|
+
return self.spawn(cmd, localenv, options.get("verbose", 0))
|
qdir/qdel.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/bin/env python3
|
|
2
|
+
"""Dequeue (remove) specified jobs from the qdir job queue"""
|
|
3
|
+
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from qdir import Queue, parser
|
|
8
|
+
|
|
9
|
+
parser.description = __doc__
|
|
10
|
+
parser.add_argument(
|
|
11
|
+
"job", nargs="+", help="The list of job Ids or job names to dequeue."
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def dequeueJobs(queue, jobList: list[str]):
|
|
16
|
+
"""Remove a list of jobIds or job names from the queue."""
|
|
17
|
+
queuedJobs: dict[Path, dict] = {}
|
|
18
|
+
queuedJobs.update(
|
|
19
|
+
[
|
|
20
|
+
(qFilePath, queue.getJobInfo(qFilePath))
|
|
21
|
+
for qFilePath in queue.getQueuedJobs()
|
|
22
|
+
]
|
|
23
|
+
)
|
|
24
|
+
if not queuedJobs and queue.verbose >= 2:
|
|
25
|
+
print("No jobs queued", file=sys.stderr)
|
|
26
|
+
for job in jobList:
|
|
27
|
+
matched = 0
|
|
28
|
+
for qFilePath, jobInfo in queuedJobs.items():
|
|
29
|
+
if job == jobInfo.get("jobId") or job == jobInfo.get("name"):
|
|
30
|
+
# Matched either jobId or job name
|
|
31
|
+
matched += 1
|
|
32
|
+
if queue.verbose >= 3:
|
|
33
|
+
print("Dequeing", queue.info.format(**jobInfo))
|
|
34
|
+
queue.dequeue(qFilePath)
|
|
35
|
+
if matched == 0 and queue.verbose >= 1:
|
|
36
|
+
print(job, "is not queued", file=sys.stderr)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def qdel(argv):
|
|
40
|
+
"""Dequeue the specified jobId numbers or job names"""
|
|
41
|
+
options = parser.parse_args(argv[1:])
|
|
42
|
+
queue = Queue(**vars(options))
|
|
43
|
+
dequeueJobs(queue, options.job)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def main():
|
|
47
|
+
qdel(sys.argv)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
if __name__ == "__main__":
|
|
51
|
+
main()
|
qdir/qstat.py
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
#!/bin/env python3
|
|
2
|
+
"""Report currrent status of the qdir job queue"""
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from qdir import Queue, default, parser
|
|
10
|
+
|
|
11
|
+
parser.description = __doc__
|
|
12
|
+
parser.add_argument(
|
|
13
|
+
"-f",
|
|
14
|
+
"--full",
|
|
15
|
+
action="store_true",
|
|
16
|
+
help="Report full detailed information in text format.",
|
|
17
|
+
)
|
|
18
|
+
parser.add_argument(
|
|
19
|
+
"-j",
|
|
20
|
+
"--json",
|
|
21
|
+
action="store_true",
|
|
22
|
+
help="Report full detailed queue status in json format.",
|
|
23
|
+
)
|
|
24
|
+
default.info = os.environ.get("QDIR_INFO", "{jobId:8} {owner:12} {status:35} {name}")
|
|
25
|
+
parser.add_argument(
|
|
26
|
+
"-i",
|
|
27
|
+
"--info",
|
|
28
|
+
help="Specify the shortened format of job information "
|
|
29
|
+
"when neither --full nor --json are specified. "
|
|
30
|
+
f"Default is environment variable QDIR_INFO or {default.info!r}",
|
|
31
|
+
default=default.info,
|
|
32
|
+
)
|
|
33
|
+
parser.add_argument(
|
|
34
|
+
"job",
|
|
35
|
+
nargs="*",
|
|
36
|
+
help="An optional list of job Ids or job names to get status. "
|
|
37
|
+
"If no jobs are specified then all currently queued jobs will be listed.",
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
if os.name == "nt":
|
|
42
|
+
|
|
43
|
+
def winOwner(self) -> str:
|
|
44
|
+
"Return name of the WindowsPath owner"
|
|
45
|
+
try:
|
|
46
|
+
import win32security
|
|
47
|
+
except ImportError:
|
|
48
|
+
return "unknown"
|
|
49
|
+
sd = win32security.GetFileSecurity(
|
|
50
|
+
str(self), win32security.OWNER_SECURITY_INFORMATION
|
|
51
|
+
)
|
|
52
|
+
owner_sid = sd.GetSecurityDescriptorOwner()
|
|
53
|
+
name, _domain, _type = win32security.LookupAccountSid(None, owner_sid)
|
|
54
|
+
return name
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
_ = Path(".").owner() # test if Path has owner method
|
|
58
|
+
except NotImplementedError:
|
|
59
|
+
Path.owner = winOwner # monkey patch if needed
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def printStatus(queue, jobList: list[str]):
|
|
63
|
+
"""Print a list of queued jobs"""
|
|
64
|
+
if queue.verbose >= 2:
|
|
65
|
+
print("Queue directory", queue.queue)
|
|
66
|
+
queuedJobs = queue.getQueuedJobs()
|
|
67
|
+
if not queuedJobs and queue.verbose >= 2:
|
|
68
|
+
print("No jobs queued")
|
|
69
|
+
jobHeadings = None
|
|
70
|
+
for qFilePath in queuedJobs:
|
|
71
|
+
jobInfo = queue.getJobInfo(qFilePath)
|
|
72
|
+
if (
|
|
73
|
+
len(jobList) == 0
|
|
74
|
+
or jobInfo["jobId"] in jobList
|
|
75
|
+
or jobInfo["name"] in jobList
|
|
76
|
+
):
|
|
77
|
+
if not jobHeadings and queue.verbose >= 1:
|
|
78
|
+
jobHeadings = {k: k.upper() for k in jobInfo}
|
|
79
|
+
print(queue.info.format(**jobHeadings))
|
|
80
|
+
print(queue.info.format(**jobInfo))
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def printDetails(queue, jobList: list[str]):
|
|
84
|
+
"""Report full details about queued jobs"""
|
|
85
|
+
if queue.verbose >= 2:
|
|
86
|
+
print("Queue directory", queue.queue)
|
|
87
|
+
queuedJobs = queue.getQueuedJobs()
|
|
88
|
+
if not queuedJobs and queue.verbose >= 2:
|
|
89
|
+
print("No jobs queued")
|
|
90
|
+
|
|
91
|
+
for qFilePath in queuedJobs:
|
|
92
|
+
jobInfo = queue.getJobInfo(qFilePath)
|
|
93
|
+
if (
|
|
94
|
+
len(jobList) == 0
|
|
95
|
+
or jobInfo["jobId"] in jobList
|
|
96
|
+
or jobInfo["name"] in jobList
|
|
97
|
+
):
|
|
98
|
+
print()
|
|
99
|
+
for key, value in jobInfo.items():
|
|
100
|
+
if "status" == key:
|
|
101
|
+
continue # Will be included with full history
|
|
102
|
+
if isinstance(value, list):
|
|
103
|
+
print(key.upper(), *value, sep="\n")
|
|
104
|
+
elif value:
|
|
105
|
+
print(f"{key.upper():10} {value}")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def printJSON(queue, jobList: list[str]):
|
|
109
|
+
"""Output detailed queue status in JSON format"""
|
|
110
|
+
jobs = {}
|
|
111
|
+
for qFilePath in queue.getQueuedJobs():
|
|
112
|
+
jobInfo = queue.getJobInfo(qFilePath)
|
|
113
|
+
if (
|
|
114
|
+
len(jobList) == 0
|
|
115
|
+
or jobInfo["jobId"] in jobList
|
|
116
|
+
or jobInfo["name"] in jobList
|
|
117
|
+
):
|
|
118
|
+
jobs[str(qFilePath)] = jobInfo
|
|
119
|
+
print(json.dumps(jobs, indent=2))
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def qstat(argv):
|
|
123
|
+
"""Report status of the queue"""
|
|
124
|
+
options = parser.parse_args(argv[1:])
|
|
125
|
+
queue = Queue(**vars(options))
|
|
126
|
+
if options.full:
|
|
127
|
+
# Report detailed information about jobs in the queue
|
|
128
|
+
printDetails(queue, options.job)
|
|
129
|
+
elif options.json:
|
|
130
|
+
# Report detailed queue status in json format
|
|
131
|
+
printJSON(queue, options.job)
|
|
132
|
+
else:
|
|
133
|
+
# Report list of jobs in queue
|
|
134
|
+
printStatus(queue, options.job)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def main():
|
|
138
|
+
qstat(sys.argv)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
if __name__ == "__main__":
|
|
142
|
+
main()
|
qdir/qsub.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
#!/bin/env python3
|
|
2
|
+
"""Submit a job (command) to the qdir job queue"""
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
from qdir import Queue, default, parser
|
|
9
|
+
|
|
10
|
+
dprefix = os.environ.get(
|
|
11
|
+
"QDIR_DPREFIX",
|
|
12
|
+
os.environ.get("PBS_DPREFIX", "(#|@?(?i:rem ))(QDIR|PBS)"),
|
|
13
|
+
) # regular expression prefix used to recognize optional file directives
|
|
14
|
+
|
|
15
|
+
parser.description = __doc__
|
|
16
|
+
default.name = os.environ.get("PBS_JOBNAME")
|
|
17
|
+
parser.add_argument(
|
|
18
|
+
"-N",
|
|
19
|
+
"--name",
|
|
20
|
+
help="Name for the job. Default is cmd[-1]",
|
|
21
|
+
default=default.name,
|
|
22
|
+
)
|
|
23
|
+
try:
|
|
24
|
+
default.interval = max(1, int(os.environ.get("QDIR_POLL_INTERVAL", "5")))
|
|
25
|
+
except ValueError:
|
|
26
|
+
default.interval = 5
|
|
27
|
+
parser.add_argument(
|
|
28
|
+
"-c",
|
|
29
|
+
"--interval",
|
|
30
|
+
type=int,
|
|
31
|
+
help="Polling interval in seconds. "
|
|
32
|
+
f"Default is environment variable QDIR_POLL_INTERVAL or {default.interval}",
|
|
33
|
+
default=default.interval,
|
|
34
|
+
)
|
|
35
|
+
parser.add_argument(
|
|
36
|
+
"-f",
|
|
37
|
+
"--foreground",
|
|
38
|
+
action="store_true",
|
|
39
|
+
help="Prevent qsub from forking to wait in background.",
|
|
40
|
+
default=False,
|
|
41
|
+
)
|
|
42
|
+
default.output = os.environ.get("QDIR_LOGNAME", "{name}._{jobId}")
|
|
43
|
+
parser.add_argument(
|
|
44
|
+
"-o",
|
|
45
|
+
"--output",
|
|
46
|
+
help="File name for combined stdout and stderr. "
|
|
47
|
+
f"Default is environment variable QDIR_LOGNAME or {default.output!r}",
|
|
48
|
+
default=default.output,
|
|
49
|
+
)
|
|
50
|
+
default.status = os.environ.get(
|
|
51
|
+
"QDIR_STATUS", "{jobId} {name} {status} after {duration}"
|
|
52
|
+
)
|
|
53
|
+
parser.add_argument(
|
|
54
|
+
"-s",
|
|
55
|
+
"--status",
|
|
56
|
+
help="Specify the format of job status messages output to terminal. "
|
|
57
|
+
f"Default is environment variable QDIR_STATUS or {default.status!r}",
|
|
58
|
+
default=default.status,
|
|
59
|
+
)
|
|
60
|
+
parser.add_argument(
|
|
61
|
+
"cmd",
|
|
62
|
+
nargs="+",
|
|
63
|
+
help="The command to enqueue plus any optional arguments. "
|
|
64
|
+
"This command must be in your PATH or explicitly specified. ",
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def parseArguments(argv: list[str]):
|
|
69
|
+
"""Parse the given list of arguments
|
|
70
|
+
|
|
71
|
+
Uses commandline arguments for top priority followed by in-file directives
|
|
72
|
+
and lastly default values.
|
|
73
|
+
|
|
74
|
+
In-file directives are the same format as commandline options and should
|
|
75
|
+
occur in comment lines at the beginning of the job script.
|
|
76
|
+
Directive lines and are preceeded by the dprefix regular expression.
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
import re
|
|
80
|
+
import shlex
|
|
81
|
+
|
|
82
|
+
# Parse commandline arguments to find cmd
|
|
83
|
+
argvNamespace = parser.parse_args(argv)
|
|
84
|
+
|
|
85
|
+
# Scan cmd[0] for optional file directives
|
|
86
|
+
directives = []
|
|
87
|
+
dprefixRe = re.compile(dprefix)
|
|
88
|
+
commentRe = re.compile("(#|@?(?i:rem|echo))")
|
|
89
|
+
try:
|
|
90
|
+
with open(argvNamespace.cmd[0]) as jobScript:
|
|
91
|
+
for line in jobScript:
|
|
92
|
+
m = dprefixRe.match(line)
|
|
93
|
+
if m:
|
|
94
|
+
directives.extend(shlex.split(line[m.end() :]))
|
|
95
|
+
elif len(line.strip()) == 0:
|
|
96
|
+
continue # ignore blank lines
|
|
97
|
+
elif commentRe.match(line):
|
|
98
|
+
continue # ignore non-directive comment lines
|
|
99
|
+
else:
|
|
100
|
+
break # otherwise stop processing
|
|
101
|
+
except OSError:
|
|
102
|
+
pass
|
|
103
|
+
directives.append("cmd")
|
|
104
|
+
_, unknown = parser.parse_known_args(directives)
|
|
105
|
+
|
|
106
|
+
# Finally, parse the combined options with priority for argv
|
|
107
|
+
options, _ = parser.parse_known_args(directives + argv)
|
|
108
|
+
|
|
109
|
+
# The cmd may only come from argv
|
|
110
|
+
options.cmd = argvNamespace.cmd
|
|
111
|
+
|
|
112
|
+
if not options.name and len(options.cmd) > 0:
|
|
113
|
+
options.name = options.cmd[-1] # default name
|
|
114
|
+
if unknown and options.verbose >= 2:
|
|
115
|
+
print("WARNING: Unknown file directives", *unknown, file=sys.stderr)
|
|
116
|
+
if options.verbose >= 5:
|
|
117
|
+
print("Arguments", argv, file=sys.stderr)
|
|
118
|
+
print("Directives", directives, file=sys.stderr)
|
|
119
|
+
print("options", options, file=sys.stderr)
|
|
120
|
+
return options
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def qsub(argv):
|
|
124
|
+
"""Enqueue a new command"""
|
|
125
|
+
options = parseArguments(argv[1:])
|
|
126
|
+
if not options.foreground:
|
|
127
|
+
cmd = [sys.executable, "-m", "qdir.qsub", "-f"] + argv[1:]
|
|
128
|
+
subprocess.Popen(cmd, start_new_session=True) # re-run in background
|
|
129
|
+
else:
|
|
130
|
+
queue = Queue(**vars(options))
|
|
131
|
+
queue.enqueue(options.cmd)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def main():
|
|
135
|
+
qsub(sys.argv)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
if __name__ == "__main__":
|
|
139
|
+
main()
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: qdir
|
|
3
|
+
Version: 1.0.0rc1
|
|
4
|
+
Summary: A very low overhead first-in, first-out job queue for the local machine
|
|
5
|
+
Author-email: Carl Osterwisch <costerwi@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/costerwi/qdir
|
|
8
|
+
Project-URL: Issues, https://github.com/costerwi/qdir/issues
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.6
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: filelock
|
|
15
|
+
Requires-Dist: pywin32; sys_platform == "win32"
|
|
16
|
+
Dynamic: license-file
|
|
17
|
+
|
|
18
|
+
# qdir
|
|
19
|
+
A very low overhead first-in, first-out job queue for the local machine
|
|
20
|
+
|
|
21
|
+
The queue is maintained as a directory of tiny job queue files.
|
|
22
|
+
When a new job is submitted, this script will identify any existing job queue files and then add its own queue file to the directory.
|
|
23
|
+
No job will begin until all the preceeding queue files are gone.
|
|
24
|
+
Each job will delete its own queue file upon completion so the next job can begin.
|
|
25
|
+
A shared queue directory can be specified on a multi-user system.
|
|
26
|
+
On Linux this is `/tmp` by default.
|
|
27
|
+
|
|
28
|
+
Any command may be submitted to the job queue using qsub (see usage below).
|
|
29
|
+
You may check the status of the queue (qstat) and remove jobs (qdel).
|
|
30
|
+
|
|
31
|
+
# Installation
|
|
32
|
+
Requires python3 running on Windows or Linux.
|
|
33
|
+
One way to get python3 on Windows is through the [Microsoft Store](https://apps.microsoft.com/detail/9PNRBTZXMB4Z).
|
|
34
|
+
Here is the command to install qdir once you have python3 available:
|
|
35
|
+
```bash
|
|
36
|
+
python3 -m pip install qdir
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
See [here](https://pip.pypa.io/en/stable/installation/) if there is "no module named pip."
|
|
40
|
+
|
|
41
|
+
## Abaqus Finite Element Analysis (FEA) extras
|
|
42
|
+
The package includes additional files to help Abaqus users submit and monitor the qdir job queue.
|
|
43
|
+
Use the following command to configure Abaqus after qdir itself has been installed:
|
|
44
|
+
```bash
|
|
45
|
+
python3 -m qdir.abaqus install
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
By default, the necessary settings are appended to the end of your user `abaqus_v6.env` file.
|
|
49
|
+
To uninstall you may removed the settings manually or with this command:
|
|
50
|
+
```bash
|
|
51
|
+
python3 -m qdir.abaqus uninstall
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
# Usage
|
|
55
|
+
|
|
56
|
+
## Command line job submission
|
|
57
|
+
Submit commands or scripts to the queue using the `qsub` command.
|
|
58
|
+
For example, on Linux:
|
|
59
|
+
```bash
|
|
60
|
+
qsub ./myJobFile.sh
|
|
61
|
+
```
|
|
62
|
+
or on Windows:
|
|
63
|
+
```bash
|
|
64
|
+
qsub myJobFile.cmd
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Any printed output from the command will be saved to a file based on the job name with the job Id appended to the end.
|
|
68
|
+
|
|
69
|
+
Display command options with `qsub -h`
|
|
70
|
+
|
|
71
|
+
## Command line queue status
|
|
72
|
+
Check status with the `qstat` command, for example:
|
|
73
|
+
```bash
|
|
74
|
+
qstat
|
|
75
|
+
```
|
|
76
|
+
or, for full details:
|
|
77
|
+
```bash
|
|
78
|
+
qstat -f
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Command line job removal
|
|
82
|
+
First use the `qstat` command above to find the jobIds and job names in the queue.
|
|
83
|
+
Delete or kill jobs with the `qdel` command followed by a jobId or job name.
|
|
84
|
+
For example, to delete job 12343:
|
|
85
|
+
```bash
|
|
86
|
+
qdel 12343
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Abaqus extras
|
|
90
|
+
Submit Abaqus jobs as usual from the command line:
|
|
91
|
+
```bash
|
|
92
|
+
abaqus job=whatever
|
|
93
|
+
```
|
|
94
|
+
This will invoke the `qdir_submit.py` module to create a job script and submit it to qdir.
|
|
95
|
+
|
|
96
|
+
You may submit jobs within Abaqus CAE by selecting `qdir` as the Job's queue before submitting.
|
|
97
|
+
You may check the queue status within CAE in the menu `Plug-ins > QDir Job Queue Monitor...`
|
|
98
|
+
Click on a Job to see more detailed information.
|
|
99
|
+
Right-click on a Job to expose the menu allowing you to delete the Job.
|
|
100
|
+
|
|
101
|
+
## Customization
|
|
102
|
+
Most parameters used by the script may be customized with command line options, system environment variables, or directives within the job file itself.
|
|
103
|
+
|
|
104
|
+
| Option | Description | Default |
|
|
105
|
+
|--------|-------------|---------|
|
|
106
|
+
| -N *NAME*, --name *NAME* | Name for the job. | The last argument of the command |
|
|
107
|
+
| -q *QUEUE*, --queue *QUEUE* | Directory to store the queue files. | environment variable `QDIR_QUEUE` or `/tmp` on Linux |
|
|
108
|
+
| -c *INTERVAL*, --interval *INTERVAL* | Polling interval in seconds. | environment variable `QDIR_POLL_INTERVAL` or 5 |
|
|
109
|
+
| -i *INFO*, --info *INFO* | Specify the format of information about jobs in queue | environment variable `QDIR_INFO` or `{jobId:8} {owner:12} {status:35} {name}` |
|
|
110
|
+
| -o *OUTPUT*, --output *OUTPUT* | File name for combined stdout and stderr. | environment variable `QDIR_LOGNAME` or `{name}._{jobId}`
|
|
111
|
+
| -s *STATUS*, --status *STATUS* | Specify the format of job status messages output to terminal. | environment variable `QDIR_STATUS` or `{jobId} {name} {status} after {duration}`
|
|
112
|
+
| -v *VERBOSE*, --verbose *VERBOSE* | Set the verbosity level. | environment variable `QDIR_VERBOSE` or 2 |
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
qdir/__init__.py,sha256=fXPPseM-MlxfFttcdmJYw0uhwTpDWrEqLZeoDN0x5r0,11072
|
|
2
|
+
qdir/qdel.py,sha256=vjDD_zBhtu4R8KkWXMNtxZg6KMXlRU8vO2gEFGHqC7k,1440
|
|
3
|
+
qdir/qstat.py,sha256=J_hsmZASpHGdeqGDFqlmwgrHEQ6N3yuwRHpWtBPHEmQ,4190
|
|
4
|
+
qdir/qsub.py,sha256=0lxm3WAfewV4ebZYufQROuoEubsA3Noq2qL33c6kbnk,4182
|
|
5
|
+
qdir/abaqus/__init__.py,sha256=AqfK9oYkq0oSbXHb-2wZmL5KxbKcvsVy4DS3wbzvkbc,113
|
|
6
|
+
qdir/abaqus/__main__.py,sha256=93NgqW2IAntLmPFFewBdFXnTpMLolm3yfkqbIQDWhws,2034
|
|
7
|
+
qdir/abaqus/qdir_plugin.py,sha256=MsP5goZO2Dqp27xIfxt00pTo6lfampHGJX-XYx40yw8,7864
|
|
8
|
+
qdir/abaqus/qdir_submit.py,sha256=3rLQlRyIWimNAfwxyGG6k50Yc9sflV0AF6s2XC2Lvhg,5349
|
|
9
|
+
qdir-1.0.0rc1.dist-info/licenses/LICENSE,sha256=MuZbvuvcRPehx5RAJMdgAuV0yvzxWaPcQGKPeihaIZY,1072
|
|
10
|
+
qdir-1.0.0rc1.dist-info/METADATA,sha256=MIRNGNHJ5yJz07OzRcieVhTsGuSwbV0j2_8FsUZO00c,4561
|
|
11
|
+
qdir-1.0.0rc1.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
12
|
+
qdir-1.0.0rc1.dist-info/entry_points.txt,sha256=zBcB1NgRBXkzcAWnk9jHgHhn1Mk2Yo-oFJFBEWymw8A,86
|
|
13
|
+
qdir-1.0.0rc1.dist-info/top_level.txt,sha256=fRgcgmSYWhMIV8wvfnG7YYMu5qS8PfIabbjXLe6Cj9E,5
|
|
14
|
+
qdir-1.0.0rc1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Carl Osterwisch
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
qdir
|