I have completed coding of a Java port of the original “Oregon Trail” text-based computer game, based on the 1978 BASIC source code. More details and the source code may be found at my github page for the project.
computers
Further New Year Updates for Forced Logouts in Programmatic MacOS Parental Controls
Early this January, 2022, Sara and I noticed that it seemed like the boys could stay on the computer much longer than what the forced time quotas my refined program enforced. Investigating the code revealed something I should have anticipated, having been part of a “Y2K readiness team” earlier in my career.
The problem was that my “UserLoginTime” objects were obtained given the output from the bash shell’s “who” command, which only shows the login time for users using a two-digit month and two-digit day, but no year. When I constructed UserLoginTimes that were holdovers from old logins at the end of December 2021, the code was assuming that it was December of the current year – nearly a year in the future. So, when checking whether the total time online for the boys was greater than the specified value, the code was subtracting a future date and time from the current date and time, resulting in a negative “elapsed time”, thus never timing out.
The original code for the constructor, with the error of always assuming the login date was in the current year, was as follows.
/**
* Constructs a UserLoginTime for the information in the passed line from
* "who" output.
*
* @param line
* a line of output from the MacOS / BSD "who" command.
*/
public UserLoginTime(final String line) {
final LocalDateTime now = LocalDateTime.now();
final int year = now.getYear();
final String[] split = line.split("[\\s]+");
this.setUser(split[0]);
this.setTty(split[1]);
final StringBuilder builder = new StringBuilder();
builder.append(year);
for (int i = 2; (i < Math.min(split.length, 5)); i++) {
builder.append(" ");
builder.append(split[i]);
}
this.setLoginTime(
LocalDateTime.parse(builder.toString(), DATE_TIME_FORMATTER));
}
To fix the problem, I simply subtract a year from the originally calculated login date, by adding the following to the end of the constructor, after the initial call to setLoginTime(…) that previously ended the constructor code, as follows:
// Check for change of year.
final LocalDateTime originalLoginTime = this.getLoginTime();
if (originalLoginTime.isAfter(now)) {
LOGGER.warn("originalLoginTime: "
+ originalLoginTime.format(DATE_TIME_FORMATTER));
this.setLoginTime(originalLoginTime.minusYears(1L));
LOGGER.warn("adjusted login time: "
+ this.getLoginTime().format(DATE_TIME_FORMATTER));
}
I suppose that, if I wanted to cover all possibilities, I would use a while loop and keep subtracting a year from the login time until the result of calling it’s “isAfter(now)” method was false. However, the kids’ login accounts would never be logged in for a span covering more than two calendar years. With power outages, OS updates, and reboots either to clear problems or to boot up an older version of MacOS to allow the boys to play an old 32-bit game, the computer wouldn’t stay online that long anyway.
One final refinement I needed to make was to handle the case where my program was unable to connect to the database, in which case it was throwing Exceptions upon startup, and never doing any kind of check. I added some code to the main application to call an alternate method if an Exception was thrown when starting up the application, by adding the Exception handling block shown below, added to the LogtimerApp’s main method.
public static void main(final String[] args) {
try {
SpringApplication.run(LogtimerApp.class, args);
} catch (final Exception e) {
LOGGER.error(
"Unable to run LogtimerApp application; attempt to check logins without using database.",
e);
LogtimerRunner.checkCurrentLoginsOnly();
}
}
I then added the following code to LogtimerRunner, recycling code I had used on the early version of the program that did not store history in the database, requiring the first refinement.
/**
* Fallback method to check only the currently logged in sessions, to be
* called when unable to obtain a database connection on startup.
*/
public static void checkCurrentLoginsOnly() {
LOGGER.info("*** checkCurrentLoginsOnly ***");
LogtimerRunner runner = new LogtimerRunner();
try {
Map<String, List<UserLoginTime>> userLogins;
userLogins = runner.getUserLogins();
if (userLogins != null) {
LogtimerRunner.checkCurrentLogins(userLogins);
}
} catch (final Exception e) {
LOGGER.error("Error checking current logins", e);
}
}
/**
* Check the given Map of users to Lists of user login times and log out
* those who have exceeded their quota.
*
* @param userLogins
* a Map of Lists of login times for each user.
*/
public static void checkCurrentLogins(
final Map<String, List<UserLoginTime>> userLogins) {
LOGGER.debug("*** User login lists ****");
final LocalDateTime now = LocalDateTime.now();
for (final Entry<String, List<UserLoginTime>> entry : userLogins
.entrySet()) {
final String user = entry.getKey();
final List<UserLoginTime> loginsForUser = entry.getValue();
final UserLoginTime lastLoginForUser =
Collections.max(loginsForUser);
LOGGER.info("*** Maximum login: " + lastLoginForUser);
final LocalDateTime fromTemp =
LocalDateTime.from(lastLoginForUser.getLoginTime());
final long minutes = fromTemp.until(now, ChronoUnit.MINUTES);
LOGGER.debug("Elapsed time: " + minutes + " minutes");
try {
(new LogtimerRunner()).logoutIfKidLoggedInTooLong(user,
minutes);
} catch (final IOException e) {
LOGGER.error("I/O error", e);
} catch (final InterruptedException e) {
LOGGER.warn("Thread interrupted", e);
}
}
}
I hope readers may find this information helpful, either if using this program, or to offer some ideas if they find themselves running into similar problems with other programs they maintain.
Adding a Dark Theme to the Biking Weather Suitability Forecast Application
Having read a few articles online about offering a dark theme as part of improving accessibility for certain people, I decided to add a dark theme option both for my website overall, and for the Biking Weather Suitability Forecast application. I will discuss what steps I took to add this to my web application; similar changes needed to be made for overrides to the default theme CSS and script files for the general website.
The easiest thing to do was to download the dark version of the “Powered by Dark Sky” icon. This will likely need to be removed in a few months, after Dark Sky stops their “grandfathered” API support at the end of 2022.
Next, I added some overrides at the bottom of my main.css file for the case when the user-specified preferred color scheme is dark, such as when choosing “dark mode” on an iPhone or Mac.
/* Dark mode color scheme */
@media(prefers-color-scheme: dark) {
body, .card, .table, caption {
background-color: black;
color: white;
}
td.status0, td.status1, td.status2, td.status3 {
color: black;
}
}
To allow writing a script that would allow changing the “Powered by Dark Sky” graphic to one with inverted colors, I added the “darksky” class to the img tag in the forecast.html page:
<a href="https://darksky.net/poweredby">
<img class="darksky" th:src="@{/img/poweredby.png}" alt="Powered by Dark Sky" style="height: 3rem;" />
</a>
Finally, I used jQuery and plain JavaScript to check if the dark mode was the preferred color scheme both on page load and on a change to the color scheme preference. If so, I called the “darkMode()” function shown below to change the image. Otherwise, if changed back to the default light mode, the default “bright” icon is restored. This was done by adding the following script to the bottom of forecast.html, after the default jquery, bootstrap, and bootstrap script includes.
<script>
function darkMode() {
$("img.darksky").attr("src",
"https://johnwatne.no-ip.biz/bikingweather/img/poweredby-darkbackground.png");
}
var mql = window.matchMedia('(prefers-color-scheme: dark)');
if (mql.matches) {
darkMode();
}
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', event => {
if (event.matches) {
darkMode();
} else {
// Restore standard / light mode image.
$("img.darksky").attr("src",
"https://johnwatne.no-ip.biz/bikingweather/img/poweredby.png");
}
})
</script>
The following images show examples of what the page now looks like in both the default light and optional dark mode. I will note that I need to fix the contrast on the linked text in dark mode. This will be an additional override that I will add soon to the CSS file.