-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathTime.java
More file actions
405 lines (365 loc) · 15.2 KB
/
Copy pathTime.java
File metadata and controls
405 lines (365 loc) · 15.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
package org.perlonjava.runtime.operators;
import org.perlonjava.runtime.runtimetypes.*;
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadMXBean;
import java.time.DateTimeException;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.format.TextStyle;
import java.util.Locale;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import static org.perlonjava.runtime.runtimetypes.ErrorMessageUtil.stringifyException;
import static org.perlonjava.runtime.runtimetypes.GlobalVariable.getGlobalHash;
import static org.perlonjava.runtime.runtimetypes.GlobalVariable.getGlobalVariable;
import static org.perlonjava.runtime.runtimetypes.RuntimeScalarCache.getScalarInt;
/**
* The Time class provides utility methods for retrieving and formatting time-related information.
*/
public class Time {
// Static scheduler for alarm functionality - using daemon threads to allow JVM exit
private static final ScheduledExecutorService alarmScheduler = Executors.newSingleThreadScheduledExecutor(r -> {
Thread t = new Thread(r, "PerlAlarmTimer");
t.setDaemon(true); // Daemon thread won't prevent JVM shutdown
return t;
});
private static ScheduledFuture<?> currentAlarmTask = null;
private static long alarmStartTime;
private static int alarmDuration;
private static Thread alarmTargetThread;
/**
* Returns the current time in seconds since the Unix epoch with second precision.
*
* @return a RuntimeScalar representing the current time in seconds.
*/
public static RuntimeScalar time() {
return new RuntimeScalar(System.currentTimeMillis() / 1000L);
}
/**
* Returns CPU time statistics for the current thread.
* In list context, returns (user, system, cuser, csys) times in seconds.
* In scalar context, returns only the user CPU time in seconds.
*
* @param ctx the context (RuntimeContextType.SCALAR or RuntimeContextType.LIST)
* @return a RuntimeList containing CPU time values
*/
public static RuntimeList times(int ctx) {
ThreadMXBean bean = ManagementFactory.getThreadMXBean();
long cpu = bean.isCurrentThreadCpuTimeSupported() ?
bean.getCurrentThreadCpuTime() : 0L;
long user = bean.isCurrentThreadCpuTimeSupported() ?
bean.getCurrentThreadUserTime() : 0L;
long system = cpu - user;
double userTime = user / 1.0E9; // user CPU time in seconds
double systemTime = system / 1.0E9; // system CPU time in seconds
RuntimeList res = new RuntimeList();
if (ctx == RuntimeContextType.SCALAR) {
// In scalar context, return just the user CPU time
res.add(userTime);
} else {
// In list context, return all four values
res.add(userTime); // user CPU time
res.add(systemTime); // system CPU time
res.add(0); // child user CPU time (not available in Java)
res.add(0); // child system CPU time (not available in Java)
}
return res;
}
/**
* Converts a given epoch time to local time and returns a formatted list of time components.
*
* @param args a RuntimeList containing the epoch time as the first element.
* @param ctx the context type, which determines the format of the result.
* @return a RuntimeList containing formatted local time components.
*/
public static RuntimeList localtime(RuntimeList args, int ctx) {
ZoneId localZone = localZoneId();
ZonedDateTime date;
if (args.isEmpty()) {
date = ZonedDateTime.now(localZone);
} else {
double dval = args.getFirst().getDouble();
if (Double.isNaN(dval) || Double.isInfinite(dval)) {
return returnUndefOrEmptyList(ctx);
}
long arg = args.getFirst().getLong();
try {
date = Instant.ofEpochSecond(arg).atZone(localZone);
} catch (DateTimeException e) {
emitTimeOverflowWarnings("localtime", arg);
return returnUndefOrEmptyList(ctx);
}
}
return getTimeComponents(ctx, date);
}
/**
* Converts a given epoch time to UTC time and returns a formatted list of time components.
*
* @param args a RuntimeList containing the epoch time as the first element.
* @param ctx the context type, which determines the format of the result.
* @return a RuntimeList containing formatted UTC time components.
*/
public static RuntimeList gmtime(RuntimeList args, int ctx) {
ZonedDateTime date;
if (args.isEmpty()) {
date = ZonedDateTime.now(ZoneOffset.UTC);
} else {
double dval = args.getFirst().getDouble();
if (Double.isNaN(dval) || Double.isInfinite(dval)) {
return returnUndefOrEmptyList(ctx);
}
long arg = args.getFirst().getLong();
try {
date = Instant.ofEpochSecond(arg).atZone(ZoneId.of("UTC"));
} catch (DateTimeException e) {
emitTimeOverflowWarnings("gmtime", arg);
return returnUndefOrEmptyList(ctx);
}
}
return getTimeComponents(ctx, date);
}
private static ZoneId localZoneId() {
RuntimeScalar tzScalar = getGlobalHash("main::ENV").elements.get("TZ");
String tz = tzScalar != null && tzScalar.getDefinedBoolean()
? tzScalar.toString()
: System.getenv("TZ");
if (tz == null || tz.isEmpty()) {
return ZoneId.systemDefault();
}
ZoneOffset posixOffset = parsePosixTzOffset(tz);
if (posixOffset != null) {
return posixOffset;
}
try {
return ZoneId.of(tz);
} catch (DateTimeException ignored) {
return ZoneId.systemDefault();
}
}
private static ZoneOffset parsePosixTzOffset(String tz) {
String s = tz.trim();
if (s.equals("UTC") || s.equals("GMT")) {
return ZoneOffset.UTC;
}
if (!(s.startsWith("UTC") || s.startsWith("GMT")) || s.length() < 5) {
return null;
}
char sign = s.charAt(3);
if (sign != '+' && sign != '-') {
return null;
}
int pos = 4;
int hourStart = pos;
while (pos < s.length() && Character.isDigit(s.charAt(pos))) {
pos++;
}
if (pos == hourStart) {
return null;
}
int hours;
int minutes = 0;
try {
hours = Integer.parseInt(s.substring(hourStart, pos));
if (pos < s.length()) {
if (s.charAt(pos) == ':') {
pos++;
}
int minuteStart = pos;
while (pos < s.length() && Character.isDigit(s.charAt(pos)) && pos - minuteStart < 2) {
pos++;
}
if (pos > minuteStart) {
minutes = Integer.parseInt(s.substring(minuteStart, pos));
}
}
} catch (NumberFormatException e) {
return null;
}
if (hours > 18 || minutes > 59) {
return null;
}
int totalSeconds = hours * 3600 + minutes * 60;
// POSIX TZ signs are inverted: UTC+9 means local time is UTC-09:00.
if (sign == '+') {
totalSeconds = -totalSeconds;
}
return ZoneOffset.ofTotalSeconds(totalSeconds);
}
// Perl's scalar gmtime/localtime returns ctime(3) format: "Sun Jan 1 00:00:00 1970"
// Do NOT use DateTimeFormatter.RFC_1123_DATE_TIME — it produces "Sun, 1 Jan 1970 00:00:00 GMT"
// which has wrong field order/format, and crashes with DateTimeException for years > 4 digits.
private static String formatCtime(ZonedDateTime date) {
String dow = date.getDayOfWeek().getDisplayName(TextStyle.SHORT, Locale.ENGLISH);
String mon = date.getMonth().getDisplayName(TextStyle.SHORT, Locale.ENGLISH);
int day = date.getDayOfMonth();
String dayStr = day < 10 ? " " + day : String.valueOf(day);
int h = date.getHour(), m = date.getMinute(), s = date.getSecond();
int year = date.getYear();
return String.format("%s %s %s %02d:%02d:%02d %d", dow, mon, dayStr, h, m, s, year);
}
private static void emitTimeOverflowWarnings(String funcName, long arg) {
String direction = arg > 0 ? "too large" : "too small";
WarnDie.warn(
new RuntimeScalar(funcName + "(" + arg + ") " + direction),
new RuntimeScalar("\n")
);
WarnDie.warn(
new RuntimeScalar(funcName + "(" + arg + ") failed"),
new RuntimeScalar("\n")
);
}
private static RuntimeList returnUndefOrEmptyList(int ctx) {
RuntimeList res = new RuntimeList();
if (ctx == RuntimeContextType.SCALAR) {
res.add(new RuntimeScalar()); // undef
}
return res;
}
private static RuntimeList getTimeComponents(int ctx, ZonedDateTime date) {
RuntimeList res = new RuntimeList();
if (ctx == RuntimeContextType.SCALAR) {
res.add(formatCtime(date));
return res;
}
// 0 1 2 3 4 5 6 7 8
// ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst)
res.add(date.getSecond());
res.add(date.getMinute());
res.add(date.getHour());
res.add(date.getDayOfMonth());
res.add(date.getMonth().getValue() - 1);
res.add(date.getYear() - 1900);
// Java DayOfWeek: 1=Mon..7=Sun; Perl wday: 0=Sun..6=Sat. The % 7 maps 7(Sun)->0, 1(Mon)->1, etc.
res.add(date.getDayOfWeek().getValue() % 7);
res.add(date.getDayOfYear() - 1);
res.add(date.getZone().getRules().isDaylightSavings(date.toInstant()) ? 1 : 0);
return res;
}
public static RuntimeScalar sleep(RuntimeScalar runtimeScalar) {
return sleepInternal(runtimeScalar, false);
}
public static RuntimeScalar sleepPrecise(RuntimeScalar runtimeScalar) {
return sleepInternal(runtimeScalar, true);
}
private static RuntimeScalar sleepInternal(RuntimeScalar runtimeScalar, boolean preciseReturn) {
RuntimeIO.flushAllHandles();
long s = (long) (runtimeScalar.getDouble() * 1000);
if (s < 0) {
getGlobalVariable("main::!").set("Invalid argument");
WarnDie.warn(
new RuntimeScalar(stringifyException(
new PerlCompilerException("sleep() with negative argument")
)),
new RuntimeScalar());
return getScalarInt(0);
}
long startTime = System.nanoTime();
try {
TimeUnit.MILLISECONDS.sleep(s);
} catch (InterruptedException e) {
// Sleep was interrupted (likely by alarm())
// Process any pending signals through the signal queue
PerlSignalQueue.checkPendingSignals();
// If the signal handler threw an exception (die), it will propagate from checkPendingSignals()
}
long endTime = System.nanoTime();
long actualSleepTime = endTime - startTime;
double sleptSeconds = actualSleepTime / 1_000_000_000.0;
return new RuntimeScalar(preciseReturn ? sleptSeconds : Math.floor(sleptSeconds));
}
/**
* Sets an alarm to go off after the specified number of seconds.
* Returns the number of seconds remaining from a previous alarm, if any.
*
* @param ctx the runtime context
* @param args the arguments (seconds to wait)
* @return a RuntimeScalar representing the seconds remaining from previous alarm
*/
public static RuntimeScalar alarm(int ctx, RuntimeBase... args) {
int seconds = 0;
if (args.length > 0) {
seconds = args[0].scalar().getInt();
}
// Calculate remaining time on previous timer
int remainingTime = 0;
if (currentAlarmTask != null && !currentAlarmTask.isDone()) {
long elapsedTime = (System.currentTimeMillis() - alarmStartTime) / 1000;
remainingTime = Math.max(0, alarmDuration - (int) elapsedTime);
currentAlarmTask.cancel(false);
}
if (seconds == 0) {
currentAlarmTask = null;
return new RuntimeScalar(remainingTime);
}
// Set up new alarm
alarmStartTime = System.currentTimeMillis();
alarmDuration = seconds;
alarmTargetThread = Thread.currentThread();
currentAlarmTask = alarmScheduler.schedule(() -> {
RuntimeScalar sig = getGlobalHash("main::SIG").get("ALRM");
if (sig.getDefinedBoolean()) {
// Queue the signal for processing in the target thread
PerlSignalQueue.enqueue("ALRM", sig);
// Interrupt the target thread to break out of blocking operations
alarmTargetThread.interrupt();
}
}, seconds, TimeUnit.SECONDS);
return new RuntimeScalar(remainingTime);
}
/**
* Check and process any pending signals.
* This method should be called at safe execution points in the interpreter.
*/
public static void checkPendingSignals() {
PerlSignalQueue.processSignals();
}
/**
* Check if an alarm is currently active.
* Used by regex engine to decide whether to use timeout wrapper.
*
* @return true if alarm is active, false otherwise
*/
public static boolean hasActiveAlarm() {
return currentAlarmTask != null && !currentAlarmTask.isDone();
}
/**
* Get the remaining time in seconds until the alarm fires.
*
* @return seconds remaining, or 0 if no alarm active
*/
public static int getAlarmRemainingSeconds() {
if (!hasActiveAlarm()) {
return 0;
}
long elapsedTime = (System.currentTimeMillis() - alarmStartTime) / 1000;
return Math.max(0, alarmDuration - (int) elapsedTime);
}
/**
* Shuts down the alarm scheduler to allow clean JVM exit.
* This method is called by the shutdown hook and can also be called manually.
*/
public static void shutdownAlarmScheduler() {
if (currentAlarmTask != null && !currentAlarmTask.isDone()) {
currentAlarmTask.cancel(false);
currentAlarmTask = null;
}
if (!alarmScheduler.isShutdown()) {
alarmScheduler.shutdown();
try {
// Wait a reasonable time for existing tasks to terminate
if (!alarmScheduler.awaitTermination(1, TimeUnit.SECONDS)) {
alarmScheduler.shutdownNow();
}
} catch (InterruptedException e) {
// Re-cancel if current thread also interrupted
alarmScheduler.shutdownNow();
// Preserve interrupt status
Thread.currentThread().interrupt();
}
}
}
}