I built it to learn minimax, the classic algorithm for game-playing computers, and noughts and crosses is the classic small case to try it on. This is the write-up for Noughts and Crosses v1, one of the Python and Flask projects on my portfolio page. The full source isn’t published, but I can show the […]
My first Sudoku solver is quick on easy puzzles and hopeless on hard ones. This post puts measured numbers on how far the later versions moved on from it. It covers the four versions on my portfolio page, from the first plain backtracking solver to the January 2024 update. The full source isn’t published, but […]
I built it to learn minimax, the classic algorithm for game-playing computers, and noughts and crosses is the classic small case to try it on. This is the write-up for Noughts and Crosses v1, one of the Python and Flask projects on my portfolio page. The full source isn’t published, but I can show the interesting part.
You can play it here. Fair warning: play fairly and you can’t win.
Noughts and Crosses v1 part-way through a game. The scores carry over between rounds.
What the app does
You type a name, press Start Game and get a three-by-three board. You are O, you click a cell, and the computer answers with an X straight away. The page keeps a running score for you and for the computer, and a Restart button starts another round without losing the scores.
The page is a handful of Jinja templates, a stylesheet and a little jQuery on the game page. Each click sends one POST to Flask, which returns the new board and whether anyone has won, drawn or neither. The browser only draws what it is told. All of the game logic is on the server.
My portfolio page invites you to “pretend you are in the 80’s and do a little bit of Wargaming”. The reference is WarGames (1983), where the computer works out from endless games of noughts and crosses that the only winning move is not to play. The joke fits, because that is more or less what happens here.
How the computer chooses its move
The opponent uses minimax, the textbook algorithm for two-player games with perfect information. It tries every legal move, then every reply to that move, and so on until the game ends, and it scores each finished game: ten for an X win, minus ten for an O win, zero for a draw. X picks the move that leads to the highest score, assuming O will always pick the reply that leads to the lowest. Here is the function:
def minimax(board, depth, is_maximizing):
score = evaluate(board)
if score == 10:
return score - depth
if score == -10:
return score + depth
if not any('_' in row for row in board):
return 0
if is_maximizing:
best_score = -1000
for i in range(3):
for j in range(3):
if board[i][j] == '_':
board[i][j] = 'X'
best_score = max(best_score, minimax(board, depth+1, not is_maximizing))
board[i][j] = '_'
return best_score
else:
# …the same loop for O, using min() and starting from 1000
Two details are worth explaining.
Depth-adjusted scoring. A win scores ten minus the number of moves it took to get there, and a loss scores minus ten plus the depth. Without that, a win in two moves and a win in six are both worth ten, and the computer might wander about for no reason. With it, the computer takes the quickest win on offer and, if it is losing, drags the game out for as long as it can. It makes the opponent look purposeful, which costs one line each way.
Why you can’t win. The board is small enough for the search to look at every possible game, so there is no heuristic and no guessing: the computer never misjudges a position. Noughts and crosses is a solved game, and with perfect play from both sides it ends in a draw. Against a perfect opponent the best you can do is draw, and any slip hands the computer the win.
The search is cheap here. Even the computer’s first reply looks at no more than about 40,000 move sequences, which Python gets through quickly, so there is no pruning and no caching. Alpha-beta pruning would matter on a bigger board; on this one it would be extra code for no visible gain.
The “user system” is a name in a cookie
The portfolio page says the app has “the starts of a user system”. To be exact, it is a name field. What you type is stored in Flask’s session, along with the board and both scores. The session is a signed cookie held in your browser, so the server keeps no state of its own: no accounts, no passwords, no database.
That has a few consequences:
Clear your cookies and the scores go with them.
Two browsers, two separate players, even if they use the same name.
Nothing is shared, so there is no leaderboard.
The data lives on the client. It is signed, but anyone can read it.
The plan was proper logins with a history of your games. Two unused files in the project are the start of it: a dashboard template that welcomes you by username, lists your game history and has a Logout button, and a script that posts each move to a numbered game. Neither is wired up. Version 1 stopped at a name in the session.
What is rough or missing
No check that a square is empty. The server never checks that the square you click is empty, and the page lets you click any square. Click one of the computer’s Xs and it becomes an O, so you can cheat your way to a win. “Unbeatable” therefore means unbeatable in a fair game. Clicks after a game has ended carry on being scored: after a win, each further click re-evaluates the same winning line and adds another point.
Restart doesn’t redraw the board. The server resets the board, but its reply doesn’t include the new one, and the page tries to redraw from that reply. The script stops with an error, so the old game stays on screen until your next click (or a reload) draws the fresh board.
Development server. The container, built on python:3.9 with Flask unpinned, runs python app.py: Flask’s built-in Werkzeug server, behind a reverse proxy. That is fine for a demo and wrong for anything with real traffic.
No tests, no README, no history. The code was only put under version control in October 2026, so I can’t show you how it evolved.
What I would do differently
Minimax was the easy part. The function is under 30 lines. The bugs all ended up in the plumbing around it: whose turn it is, which squares are free, when a game is over, what Restart sends back and how the score is kept. Both of the flaws at the top of that list live there, not in the algorithm. If I were starting again, I would give that game-state code as much care as the clever bit.
Have a go
If you can get a draw, you’ve played as well as anyone can against it. Try it at the live demo, and if you want the other projects, they’re all on the portfolio page. Good luck!
My first Sudoku solver is quick on easy puzzles and hopeless on hard ones. This post puts measured numbers on how far the later versions moved on from it. It covers the four versions on my portfolio page, from the first plain backtracking solver to the January 2024 update. The full source isn’t published, but the interesting parts are quoted below.
It started as an algorithm challenge: I wanted to write a solver and then keep making it faster. The Flask page was a way to show each version working.
What the project is
Each version is a small Flask app with a nine-by-nine grid of number inputs. You fill in the givens, press the button and the server either shows the solved grid or says the puzzle can’t be solved. The apps run in Docker (python:3.8-slim-buster images, Flask unpinned, the Werkzeug development server), one container per version, behind a reverse proxy on cosmo.ihelm.org.uk. All four are still live:
Sudoku Solver v2 (January 2024 update) after solving a puzzle. The iteration counter on each version counts something different, which matters later.
Versions 1 and 1.1 date from 22 and 23 January 2023, and version 2 followed on 29 January 2023. The January 2024 update came almost a year later, on 21 January 2024.
Version 1: plain backtracking
Version 1 is the textbook approach. Find the first empty cell, reading row by row. Try each digit from 1 to 9. If a digit doesn’t clash with anything, place it and recurse. If the recursion fails, take the digit back out and try the next one. When no digit fits, return and let the previous cell try something else.
def solve(grid):
global iterations
empty = find_empty(grid)
if not empty:
return True
row, col = empty
for num in range(1, 10):
if is_valid(grid, (row, col), num):
iterations += 1
grid[row][col] = num
if solve(grid):
return render_template("solution.html", grid=grid, iterations=iterations)
grid[row][col] = 0
return False
def find_empty(grid):
for row in range(9):
for col in range(9):
if grid[row][col] == 0:
return (row, col)
return None
def is_valid(grid, pos, num):
# Check row
for i in range(9):
if grid[pos[0]][i] == num:
return False
# Check col
# …
It works, but it is crude, and it has rough edges I’d rather admit to than hide:
is_valid rescans the row, the column and the box for every digit tried.
There is no check on the input. Give it a grid with a repeated given and it searches the whole tree before saying “Puzzle cannot be solved”.
solve returns render_template(...) from inside the recursion. On success, the solution page is rendered at every level on the way back up. It is wasteful.
The iteration counter is never reset. On the live server it accumulates across visitors until the container restarts.
Version 1.1: a better page, the same algorithm
Version 1.1 is the same solver with a better page: a styled grid with the three-by-three boxes marked, a proper title and a meta description, the first bit of SEO. Nothing changed in the search. I include it because the portfolio lists it, and because it makes a useful control: any difference between 1 and 1.1 is not about the algorithm.
Version 2: validate first, then try only what fits
Version 2 made two changes. First, is_input_valid checks the givens up front and rejects a grid with repeated digits (“Puzzle cannot be solved as the input is invalid”) instead of searching for a solution that can’t exist.
Second, solve stopped trying every digit. Trying digits that couldn’t possibly fit looked wasteful, so cutting them out was the next obvious speed-up. It builds the set of candidates for the cell from its row, column and box, and tries only those:
possibilities = {1, 2, 3, 4, 5, 6, 7, 8, 9}
for i in range(9):
if grid[row][i] in possibilities:
possibilities.remove(grid[row][i])
if grid[i][col] in possibilities:
possibilities.remove(grid[i][col])
box_x = col // 3
box_y = row // 3
for i in range(box_y*3, box_y*3 + 3):
iterations += 1
for j in range(box_x * 3, box_x*3 + 3):
if grid[i][j] in possibilities:
possibilities.remove(grid[i][j])
for num in possibilities:
if is_valid(grid, (row, col), num):
# …
I described this on the portfolio page as drastically reducing solving time. The measurements below don’t bear that out, and the reason is worth understanding. Version 2 still picks the first empty cell in row order, and its candidates come out in ascending order. Removing the digits that is_valid would have rejected anyway doesn’t change which digits get tried, in which order, so it explores exactly the same search tree as version 1. It does less work per step, because it doesn’t call is_valid for digits already ruled out. But it takes exactly as many steps.
Version 2, January 2024 update: fewest candidates first
This is the version where the search itself changed. (Its own page calls it “Version 4”; I’ve kept the portfolio’s name for it here.) There are two ideas.
Pick the most constrained cell, not the first one.find_least_possibilities looks at every empty cell, works out its candidates and returns the cell with the fewest. This is the minimum remaining values heuristic (MRV), sometimes called “fail first”. A cell with one candidate is forced, so you place it immediately. A cell with zero candidates means you have already gone wrong, and you find out at once instead of after filling in a dozen cells that never mattered.
def find_least_possibilities(grid):
min_count = 10 # More than the maximum possible count (9)
best_cell = None
for row in range(9):
for col in range(9):
if grid[row][col] == 0:
possibilities = find_possibilities(grid, row, col)
if len(possibilities) < min_count:
min_count = len(possibilities)
best_cell = (row, col)
return best_cell
Check the consequences of each move straight away. After placing a digit, forward_check looks at every empty cell, and if any of them has been left with no candidates, the move is rejected without recursing at all:
def forward_check(grid, row, col, num):
original_num = grid[row][col]
grid[row][col] = num # Temporarily place the number
for i in range(9):
for j in range(9):
if grid[i][j] == 0: # Check only empty cells
if not find_possibilities(grid, i, j):
grid[row][col] = original_num # Restore the original number
return False
grid[row][col] = original_num # Restore the original number
return True
The cost is that each call now does much more work. Choosing a cell scans every empty cell, and forward checking scans them all again for every digit tried. Fewer calls only pays off if the saving outweighs that, and as the results show, on easy puzzles it makes no visible difference. This version keeps is_input_valid from version 2.
Both ideas were my own, worked out the way a person tackles a Sudoku by hand: start with the cell that has the fewest options, and notice straight away when a move leaves another cell with none.
How I measured it
The apps’ own iteration counters can’t be compared. Version 1 counts only the digits that pass is_valid, and never resets. Version 1.1 counts every digit it tries. Version 2 adds three on each call to solve, because the increment sits inside the loop over the box’s rows. The January 2024 version counts only the digits it places. So the “approximately 1/100th” on my portfolio page, comparing the January 2024 version with version 2, was never a like-for-like figure. The rest of this post is the like-for-like version.
For this post I wrote a small test harness that sends the same puzzles to all four apps through Flask’s test client and counts one common unit: the number of calls to solve, recursive calls included. Every run has a 60 s limit, and a run that hits it is stopped and reported as not finished. The harness checks every answer independently against the puzzle’s givens, rather than trusting the app, and it repeats each finished run and takes the median time. The times come from my PC, not the server, and the demos run on a different Python version, so treat them as relative to one another rather than as server figures. Timings of a few milliseconds vary noticeably from run to run, so I make no claims about differences between them.
Built against backtracking (17 clues): the puzzle illustrating Wikipedia’s Sudoku solving algorithms article, described there as designed to work against the brute-force algorithm. I read the digits from the image.
Valid givens, no solution (33 clues): I built this from the Easy puzzle by adding a 5 in the first cell, where the real solution has a 4. The 5 repeats nothing already given in its row, column or box, so only a search can find the problem.
Duplicate givens (33 clues): I built this from the Easy puzzle by adding a second 3 to the first row.
Results
Solve calls and outcome per version. Times are shown only where the run was long enough for them to mean anything. Every other run on Easy, no solution and duplicate givens finished in a few milliseconds.
Puzzle
v1
v1.1
v2
v2 (January 2024)
Easy
201, solved
201, solved
201, solved
50, solved
Hard
335,638, solved, 1.58 s
335,638, solved, 1.64 s
335,638, solved, 1.32 s
3,670, solved, 0.406 s
Minimal (17 clues)
did not finish in 60 s (13,463,698 reached)
did not finish in 60 s (12,996,226 reached)
did not finish in 60 s (15,440,586 reached)
4,113, solved, 0.530 s
Built against backtracking
did not finish in 60 s (13,369,072 reached)
did not finish in 60 s (12,981,751 reached)
did not finish in 60 s (10,908,674 reached)
39,223, solved, 4.72 s
Valid givens, no solution
349, unsolvable
349, unsolvable
349, unsolvable
4, unsolvable
Duplicate givens
308, unsolvable
308, unsolvable
0, rejected as invalid
0, rejected as invalid
What the table shows
Versions 1, 1.1 and 2 search exactly the same tree. Wherever they finish, they make the identical number of calls: 201 on Easy and 335,638 on Hard. Version 2 only makes each call cheaper. On Hard that gets it from 1.58 s to 1.32 s, but it is the same search.
On easy puzzles, the January 2024 version makes fewer calls but is no faster. On Easy it needs 50 calls against 201, yet all four versions finish in a few milliseconds and I wouldn’t claim a winner. This is the trade-off from earlier: a smarter call is a more expensive call.
On a hard puzzle, the call count wins. On Hard, the January 2024 version makes 3,670 calls against 335,638. Version 2 makes 91.5 times as many calls, and takes 3.24 times as long. Version 1 takes 3.88 times as long. The time ratio is smaller than the call ratio because each of the January 2024 version’s calls does far more work. It is still a large gain.
On the hardest puzzles, it is the difference between solving and not solving. The 17-clue puzzles give a plain first-empty-cell search very little to work with. With so few givens, the early cells have many candidates and the contradiction may only appear many levels deeper, so the search spends its time in huge subtrees that contain no solution. Versions 1, 1.1 and 2 didn’t finish either one within 60 s. The January 2024 version solved the first in 0.530 s and the Wikipedia puzzle, built specifically to defeat brute force, in 4.72 s.
Early rejection pays off on impossible puzzles. With the valid-looking but unsolvable puzzle, the first three versions make 349 calls to discover there is no solution, while the January 2024 version needs 4. Duplicate givens cost version 1 and 1.1 308 calls, and cost versions 2 and later none at all, because the input check rejects them first.
Where the old 1/100th claim stands
The portfolio page says the January 2024 version “reduces solution attempts to approximately 1/100th of the v2 algorithm”. Measured as solve calls, on Hard, version 2 makes 91.5 times as many, so the claim is close there. On Easy it is 4.02 times. On the two hardest puzzles version 2 doesn’t finish at all, so there is no ratio to quote. “Approximately 1/100th” is a fair description of one puzzle and an overstatement for easy ones.
I’ve corrected the portfolio page to quote the measured figure.
Lessons
Three things I’d pass on from this:
Measure before you claim, and measure like for like. My own “approximately 1/100th” was read off counters that counted different things.
Doing less work per step and taking fewer steps are different improvements. Version 2 did the first and the January 2024 version did the second.
A cleverer search is not always faster. Where the puzzle is easy, the extra work per call cancels the saving.
Wrapping up
That is the whole story of the four versions, from a solver that falls over on a 17-clue puzzle to one that solved every puzzle I gave it. If you want to compare them yourself, the four demos are all still live. Try the same hard puzzle on version 2 and the January 2024 update and watch which one comes back. Good luck, and let me know if you find a puzzle that stumps the last one.
Welcome to my collection of hobby software projects. This page catalogues various programming experiments and tools I’ve developed in my spare time. The Sudoku solver and Noughts and Crosses now have their own write-ups, with more detailed explanations of the code and methodology, linked below. The source code isn’t published. You can explore each project by following the links below, which will take you to interactive demonstrations.
Hosting Infrastructure
Each project is containerised and hosted using Docker images running on a dedicated server. While I make these available for demonstration purposes, please note:
These projects are hosted on modest hardware with no uptime guarantees
Functionality may occasionally change, typically to fix issues or improve the user experience
The original code is generally preserved when creating new versions, allowing for comparison between iterations
Update (5 February 2024): Due to increasing popularity, all tools have been migrated to a dedicated server running a reverse proxy system. This infrastructure upgrade means all projects now use secure connections (HTTPS) and are served from my domain rather than direct IP addresses.
I built this one just for fun: a light-hearted toy, and the simplest of the projects.
A simple fortune-telling toy: type a question, press Ask and the ball answers
It has the 20 classic answers: 10 positive, 5 neutral and 5 negative
It never repeats either of the last two answers. That memory is shared by every visitor, so it is the last two answers given to anyone, not just to you
Your question is shown back on the answer page but plays no part in choosing the answer
Same solver as v1 with two additions: a check that rejects a puzzle with repeated givens, and a set of candidates for each cell so it only tries digits that can still fit
Explores exactly the same search tree as v1, so it makes the same number of solve calls (335,638 on my Hard test puzzle). Each call is cheaper, but the search is no shorter
Keeping your software up-to-date is crucial for optimal performance and security on your Windows machine. Traditionally, this involved visiting individual websites or manually checking for updates within each program. Thankfully, Windows 11 offers a more streamlined approach through the Windows Package Manager (winget).
In this post, I’ll show you how to use the powerful winget upgrade command with the --all flag to effortlessly update all your installed programs at once. I’ll also share some alternative commands that give you more control over the update process.
Updating Everything with winget upgrade --all
The winget upgrade --all command is like a one-stop shop for updating all your installed applications through winget. Here’s how to use it:
Open Command Prompt: Press the Windows key + R, type “cmd,” and press Enter.
Execute the Command: Type winget upgrade --all and press Enter.
That’s it! Winget will scan your system for software updates and, after prompting for confirmation (if applicable), proceed with the download and installation process. It really is that simple.
Beyond --all: Exploring Other winget upgrade Commands
While the --all option is incredibly convenient, sometimes you might want a more targeted approach. Here are some useful variations of the winget upgrade command that I’ve found helpful:
winget upgrade <package-id>: This updates a specific program. Find the package ID using winget list.
winget upgrade --source <source-name>: Updates from a specific software repository (source).
winget upgrade --include-unknown: Upgrades even uninstalled applications with available updates.
winget upgrade --silent: Performs the upgrade silently, without prompting for confirmation. (Use with caution!)
Additional Tips for Power Users
If you really want to get the most out of winget, here are a few extra tips I’ve picked up:
Schedule Updates: Create a scheduled task to run winget upgrade --all periodically for automated updates. This is perfect if you want to keep things up-to-date without having to remember to run the command.
Review Updates First: Before running --all, consider reviewing the update list with winget upgrade --info to identify any potential conflicts. This can save you headaches if certain updates might cause problems with your workflow.
By leveraging these winget upgrade commands, you can streamline your software update process, ensuring a secure and optimized Windows environment. I’ve found this approach saves me tons of time compared to the old manual update method.
Just remember to implement these commands with caution, especially when using the --silent option or when updating critical software. A quick review before hitting Enter can prevent unexpected issues!
Have you tried using winget to manage your software updates?
Sieve scripts — a tool used in Plesk servers outfitted with Dovecot and Warden Anti-Spam — which can lead to rules that filter emails in unexpected ways. If you’ve noticed emails mysteriously disappearing or ending up in spam folders when they shouldn’t, Sieve scripts could be the culprit worth investigating.
The Role of Sieve Scripts in Email Management
Sieve scripts offer a robust mechanism for filtering emails on the server side. They empower users to automate actions such as sorting emails into designated folders, setting up auto-replies, and filtering out spam. Implemented on the server, these scripts process emails before they even hit your email client, serving as a critical defence line against unwanted communications.
A Case Study: The Journey to INBOX.Spam
A recent examination revealed an interesting case where an email, marked as clean by the content filter Amavis, was nonetheless directed to the INBOX.Spam folder. The culprit? A specific fileinto action within a Sieve script, instructing Dovecot to categorise the email as spam. This instance highlights the significant impact Sieve scripts can have on email routing.
How to Access and Modify Sieve Scripts in Plesk
Plesk server users can manipulate Sieve scripts through the web interface or by shell access for those requiring a deeper level of customization. Within the Plesk interface, the “Mail” section offers a gateway to adjusting spam filter settings and managing rules at the individual email account level. For more advanced modifications, including direct Sieve script edits, shell access will be necessary. This approach demands a good grasp of Linux commands and familiarity with Dovecot’s structure.
Sieve Script File Paths and Patterns
Sieve scripts are typically located in the user’s mail directory, with paths varying based on the server’s configuration. Common locations include:
This rule directs emails from example@domain.com straight to spam, showcasing how specific patterns in Sieve scripts govern email sorting.
Adjusting Filtering Rules
The Sieve script shared earlier delineates various rules for email handling, from whitelisting certain addresses to blacklisting others, and employing spam filters based on the X-Spam-Level header. For instance, it includes conditions to file emails from specific senders into INBOX.Spam, underlining the necessity of precise rule configuration to avoid ensnaring legitimate emails.
Best Practices for Sieve Script Management
Review Rules Periodically: Keep an eye on your Sieve script rules, especially after changes in email communication patterns.
Proceed With Caution: Be careful when modifying Sieve scripts! Small syntax errors can have big consequences for email delivery.
Back Up Before Modifying: Always back up your existing scripts before making adjustments:
Use Comments for Clarity: Add comments to remember why you created certain rules:
# Rule to handle marketing emails
if header :contains "Subject" "Newsletter" {
fileinto "INBOX.Marketing";
}
Monitor After Changes: After tweaking rules, keep an eye on your email for a few days to make sure everything’s working as expected.
Troubleshooting Common Issues
If emails seem to go missing, first check your spam folder! It’s amazing how often this simple step resolves the mystery.
For syntax issues after editing, you can verify your script with:
sievec /path/to/script.sieve
If your changes don’t seem to take effect, check that the file ownership matches the mail user and consider restarting Dovecot to reload scripts.
Efficient email management hinges on a fine-tuned approach to spam filtering. By getting comfortable with Sieve scripts on your Plesk server, you can take control over which emails land in your inbox versus spam. Regular reviews and cautious customisation of these scripts will ensure your email system remains effective and secure, tailored to your specific needs.
Expanding your Debian server’s root partition without downtime is a critical skill for managing server resources efficiently, especially when you’re using Logical Volume Manager (LVM) for disk management. This guide provides a detailed walkthrough, including command outputs for clarity and copy-paste commands for ease of use.
Prerequisites
Before beginning the partition expansion process:
Ensure you have backed up all critical data
Verify you have root access or the ability to use sudo
This guide assumes familiarity with basic Linux terminal commands
Your system should be using LVM for volume management
Step 1: Verify Current Disk Layout
Identify your disk’s current layout to determine the partition you need to resize. Use lsblk to list all block devices and their mount points:
lsblk
Example output:
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
sda 8:0 0 160G 0 disk
├─sda1 8:1 0 487M 0 part /boot
└─sda2 8:2 0 20G 0 part
├─vg00-lv01 254:1 0 8G 0 lvm /
└─vg00-lv00 254:0 0 2G 0 lvm [SWAP]
This output shows:
A 160GB disk (sda)
A boot partition (sda1) of 487MB
An LVM partition (sda2) of 20GB, containing:
An 8GB root logical volume (vg00-lv01)
A 2GB swap logical volume (vg00-lv00)
Step 2: Resize the Partition with fdisk
Caution: Deleting and recreating the partition must be done carefully to avoid data loss.
Launch fdisk on Your Disk:
bash
sudofdisk /dev/sda
Delete the Existing Partition:
First, print the partition table:
Command (m for help): p
Example output:
Disk /dev/sda: 160 GiB, 171798691840 bytes, 335544320 sectors
Disk model: Virtual Disk
Units: sectors of 1 * 512 = 512 bytes
Device Boot Start End Sectors Size Id Type
/dev/sda1 * 2048 999423 997376 487M 83 Linux
/dev/sda2 999424 20971519 19972096 20G 8e Linux LVM
Note the Start sector of /dev/sda2. Delete /dev/sda2:
Command (m for help): d
Partition number (1,2, default 2): 2
Recreate the Partition:
Create a new primary partition:
Command (m for help): n
Partition type
p primary (1 primary, 0 extended, 3 free)
e extended (container for logical partitions)
Select (default p): p
Partition number (2-4, default 2): 2
First sector (999424-335544319, default 999424): 999424
Last sector, +/-sectors or +/-size{K,M,G,T,P} (999424-335544319, default 335544319):
Important: Use the same starting sector as noted earlier (999424 in this example).
If prompted about removing the signature, choose No:
Partition #2 contains a LVM2_member signature.
Do you want to remove the signature? [Y]es/[N]o: N
Set the Partition Type:
Change the partition type to Linux LVM:
Command (m for help): t
Partition number (1,2, default 2): 2
Hex code (type L to list all codes): 8e
Write Changes and Exit:
Command (m for help): w
Step 3: Reboot the System
After modifying the partition table, reboot your system:
sudoreboot
This step is necessary for the kernel to recognise the new partition table.
Step 4: Resize the Physical Volume
Once the system has rebooted, notify LVM of the partition’s new size:
sudo pvresize /dev/sda2
This command adjusts the physical volume to use the newly expanded partition space.
Step 5: Extend the Logical Volume
Increase the size of the logical volume to use all available space:
sudo lvextend -l +100%FREE /dev/vg00/lv01
This command allocates all available free space in the volume group to the logical volume.
Step 6: Resize the Filesystem
Finally, resize the filesystem on the logical volume to use the newly allocated space.
For ext4 filesystem:
sudo resize2fs /dev/vg00/lv01
For xfs filesystem:
sudo xfs_growfs /dev/vg00/lv01
Verification
After completing all steps, verify that your root partition has been successfully expanded:
df -h /
This will show the new size of your root filesystem.
Troubleshooting Common Issues
Issue: Unable to Delete Partition
If you receive errors when trying to delete the partition, ensure no process is using it:
lsof|grep sda2
Issue: LVM Not Recognising New Size
If pvresize doesn’t recognise the new size, try rescanning the SCSI bus:
echo1> /sys/class/block/sda/device/rescan
Issue: Filesystem Not Expanding
If the filesystem doesn’t expand despite successful LV extension, check the filesystem type:
df -T /
And use the appropriate resize command based on the filesystem type.
Conclusion
You’ve now successfully expanded your root partition to utilise additional disk space, enhancing your Debian server’s capacity without significant downtime. Remember, the key to a smooth operation is careful planning and ensuring you have backups before starting.
This technique is particularly useful for virtual machines where disk space can be allocated dynamically, allowing you to adjust storage resources as your requirements grow.
If you run a server hosting multiple websites, you may encounter issues with WordPress installations due to a feature called XML-RPC. This remote admin functionality allows third-party applications to connect to WordPress sites and post new articles or perform other administrative actions.
While XML-RPC is excellent in theory, in practice it has become a significant avenue for attack by bot networks. Server administrators often observe hundreds or thousands of malicious connection attempts per minute across multi-domain hosting environments. On one of our servers hosting 40 sites (nearly all WordPress), these attacks are a constant concern.
Impact on Server Performance
These malicious XML-RPC requests consume valuable server resources even when they fail to compromise a site. With multiple WordPress installations managed by people with varying technical skills, some sites may become vulnerable due to outdated software or poor maintenance practices.
The cumulative impact of these requests can significantly degrade server performance, causing:
Increased response times for legitimate visitors
Higher CPU and memory usage
Potential service disruptions during peak attack periods
Premature need for hardware upgrades
Implementing a Solution with mod_security
If you’re running mod_security (which is strongly recommended for protecting your sites and your clients’ sites), you can add the following rule to block malicious XML-RPC requests:
#Block requests to xmlrpc.php with no referring URL
SecRule REQUEST_METHOD "POST" "deny,status:401,id:5000900,chain,msg:'xmlrpc.php request blocked, no referer'"
SecRule &HTTP_REFERER "@eq 0" "chain"
SecRule REQUEST_URI "xmlrpc.php"
Once implemented, any request to xmlrpc.php that doesn’t originate from a link or trigger from a legitimate website (i.e., has no referrer) will be blocked with a 401 (Forbidden) response. This effectively stops the vast majority of automated attacks whilst allowing legitimate XML-RPC usage.
Enhanced Protection with Firewalls
For additional protection, if you have a firewall or IP banning software such as CSF Firewall or Fail2ban, you can configure these tools to scan your log files and automatically ban repeat offenders that trigger this mod_security rule.
This layered security approach provides both immediate request blocking and longer-term protection against persistent attackers.
When This Approach May Not Be Suitable
This solution may not be appropriate in all scenarios, particularly:
If you or your customers actively use third-party services that utilise the XML-RPC functionality
When legitimate applications need programmatic access to WordPress without a referrer
If you have specific IPs that need to access XML-RPC (such as from a content management system or publishing tool), you could modify the rule to exclude those IP addresses. However, this approach may become difficult to maintain over time, especially with services that don’t use fixed IP addresses.
Alternative Approaches
If the solution above doesn’t suit your needs, consider these alternatives:
Selective enabling: Only enable XML-RPC on specific WordPress sites that genuinely need it
Plugin solutions: Use security plugins that provide more granular control over XML-RPC access
IP whitelisting: Configure your web server to only allow XML-RPC requests from trusted IP addresses
Request throttling: Implement rate limiting for XML-RPC endpoints
Conclusion
By implementing this mod_security rule, you can significantly reduce server load and improve security across multiple WordPress installations. The approach is particularly valuable for shared hosting environments where WordPress sites of varying ages and maintenance levels coexist.
Monitor your server logs after implementation to ensure legitimate services aren’t being inadvertently blocked, and adjust your rules as needed to balance security and functionality.
By default, cPanel places its log files in /usr/local/apache/domlogs/DOMAIN. These logs are split up by domain, which keeps things organised but can make troubleshooting difficult when you need to identify patterns across multiple domains or determine where a specific problem might lie.
The Challenge with Separate Domain Logs
When managing a server with multiple websites, having logs separated by domain creates several challenges:
You need to check multiple files when troubleshooting server-wide issues
It’s difficult to identify patterns that might affect several domains
When investigating suspicious activity, you can’t easily see if it’s targeting multiple sites
Resource usage analysis becomes more complex
Creating Combined Logs
While combining logs can be done with a bash script that concatenates existing files, this approach is time-consuming and doesn’t provide real-time insight. A more efficient solution is to modify the Apache configuration to create a new combined log file that captures all domain activity in a single location.
Edit Your vhost.default File
For Apache 2.4 (the current version at the time of writing), you’ll need to edit the vhost template file:
This configuration creates a custom log format called “vcommon” with the following components:
%V – The server name according to the UseCanonicalName setting
%a – Client IP address
%l – Remote logname (usually ‘-‘)
%u – Remote user if authenticated
%t – Time the request was received
\"%r\" – First line of request (method, path, protocol)
%s – Status code
%b – Size of response in bytes
\"%{Referer}i\" – Referer header
\"%{User-agent}i\" – User-agent header
The %V at the beginning is particularly important as it identifies which domain the log entry is associated with, allowing you to still filter by domain when needed while keeping everything in one file.
Rebuilding the Apache Configuration
To implement these changes, you need to rebuild the Apache configuration and restart the service:
After completing these steps, a new combined log file will be created at /usr/local/apache/logs/vhost-access_log that contains entries from all your domains.
SSL Considerations
If you want to track SSL accesses in the combined log as well, you’ll need to make a similar modification to the SSL vhost template. However, as the author notes, this may not be necessary if you have a limited number of SSL sites, as they tend to stand out more in normal monitoring.
And add the same LogFormat and CustomLog lines as above.
Log Rotation Considerations
Remember that creating a new log file requires proper log rotation configuration to prevent it from growing too large. This topic will be covered in a future post, but it’s important to implement log rotation for any new log files you create.
A basic approach would be to add an entry to /etc/logrotate.d/apache to ensure your new combined log is rotated alongside the standard Apache logs.
Benefits of Combined Logs
With this configuration in place, troubleshooting becomes significantly easier:
One file to tail or grep instead of dozens
Ability to see patterns across multiple domains
Easier identification of distributed attacks targeting multiple sites
Simplified log analysis with tools like AWStats or GoAccess
This simple configuration change can save considerable time and effort when managing a multi-domain cPanel server, making it a valuable addition to your server administration toolkit.
Apache is a powerful web server, and cPanel offers excellent management capabilities, but when you need to diagnose what’s consuming resources or causing slowdowns, the built-in tools can be limited. ApacheTop helps by providing real-time analysis of your web server traffic.
One of the most useful diagnostic methods is examining raw Apache logs. However, if you’re managing a busy site or a server with dozens (or more) domains, ApacheTop becomes an invaluable tool for getting a clear picture of what’s happening on your server.
cPanel Preparation
If you’re using cPanel, I recommend combining your domain logs first to make analysis more straightforward. You can find instructions for combining Apache logs here. While there are alternative methods for consolidating logs, any approach that combines your logs will work with ApacheTop.
Installation on CentOS
The following installation instructions are specific to CentOS, though you can install ApacheTop on most Linux distributions with similar package management commands.
Enable EPEL Repository
First, enable the EPEL (Extra Packages for Enterprise Linux) repository. This gives you access to third-party repositories not directly maintained by CentOS. As with any third-party repository, appropriate caution is warranted.
Run the following command:
yum install epel-release
If this command doesn’t work, search for the appropriate epel-release RPM package for your version of CentOS.
Note: Installing this package will clear your YUM cache files, so the next command may take a few minutes longer than usual to complete.
Install ApacheTop
Once EPEL is enabled, installing ApacheTop is straightforward:
yum install apachetop
That’s all there is to the installation process!
Using ApacheTop
To run ApacheTop, simply execute the command and direct it to your Apache log file. The tool will begin reading logs from that point onwards. Press the space bar to refresh the statistics more frequently than the default interval.
While using ApacheTop, press ? to access the help menu for a complete list of commands and options.
Here are some particularly useful features:
Press d to cycle through different statistical views
The host view displays the number of hits and data transfer for each host, making it easy to identify poorly behaving clients
The referrer view shows where your traffic is coming from—especially useful if a site has received publicity and is experiencing heavy traffic
For the most meaningful analysis, I recommend leaving ApacheTop running for at least 5 minutes to gather sufficient data before evaluating the results.
Practical Applications
ApacheTop can help you:
Identify individual IP addresses causing excessive traffic
Determine which pages or resources are most frequently requested
Monitor bandwidth consumption in real-time
Detect potential DDoS attacks or unusual traffic patterns
Optimise your server configuration based on actual usage patterns
Conclusion
ApacheTop is a lightweight yet powerful tool for real-time monitoring of Apache web servers. By providing immediate insights into server activity, it helps administrators quickly identify and address performance issues before they impact users.
For cPanel administrators, combining this tool with consolidated Apache logs creates an efficient monitoring solution that makes troubleshooting significantly easier.
In the last two weeks, a new hack has made the rounds. This exploits a weakness in either the hosting or the security of a WordPress blog and adds malicious code to every JavaScript file (.js). The code can vary a bit but typically starts like this (with a unique identifier):
As this malicious code gets injected into all JavaScript files, it’s particularly tricky to remove. To date, the normal security tools do not detect this vulnerability (including Sucuri), but I am sure with the next major update they will incorporate detection.
The exact cause isn’t yet known, but most likely it’s due to out-of-date plugins which allow unauthorised modification of files. Once that vulnerability is exploited, the infection seeks out JavaScript files and appends the malicious code to the end of each one.
Cleaning Your WordPress Installation
Cleaning the infected files is not easy; in-situ it is almost impossible. If you have access to a Linux-based computer or a Mac (based on FreeBSD), these instructions can help you clean up your site. You should still update your plugins and WordPress version afterwards to prevent re-infection.
Step 1: Backup
Download all your files, including everything, to ensure a complete eradication is possible. Once downloaded, compress the files into a ZIP archive (keep a copy to work on; the ZIP is there in case you need to restore anything).
Step 2: Clean
The easiest way to clean is to restore an old backup from before the infection. If you have one, upload it (overwriting existing files) and proceed to the hardening step below.
If you don’t have a backup, or your backup was also compromised, you can try using the sed command. This command differs slightly between macOS and Linux:
For macOS (FreeBSD) users:
LC_ALL=C find /PATH_TO_YOUR_HACKED_FILES/ -type f -exec sed -i '' -E "s/(\/\*.{32}\*\/\;window\[.*\/\*.{32}\*\/)/ /"{} +
The LC_ALL=C prefix ensures it will work on Macs with a non-US locale. The expression looks for the starting window string with the unique comment identifier, then finds the matching ending comment and removes everything in between.
For Linux users:
find /PATH_TO_YOUR_HACKED_FILES/ -type f -exec sed -i -E "s/(\/\*.{32}\*\/\;window\[.*\/\*.{32}\*\/)/ /"{} +
Note that for Linux, you remove both the LC_ALL=C prefix and the empty quotes ('') after -i. In FreeBSD (macOS), these empty quotes prevent the creation of backup files, which aren’t needed in this case.
This approach could potentially affect legitimate code; however, as the malicious code is deliberately obfuscated, it’s generally safer to remove anything matching this pattern that you can’t easily read or verify.
Step 3: Upload & Hardening
Now upload your cleaned files, overwriting all existing files. Your site is (for now) clean. To harden your WordPress installation against future attacks, I recommend the following steps:
Update your WordPress core to the latest version
Update your themes (a more common vector for attack than most people realise)
Update all your plugins to their latest versions
Finally, install security plugins to help prevent modifications. These can also warn you if someone logs into your blog and alert you to other important changes.
I recommend Sucuri Security. It has a free version with premium additions. The free version is sufficient for most sites, but if your site generates revenue, I recommend considering the paid version for enhanced protection.
Once installed, go through the setup process and enable as many protective features as feasible for your site, especially directory protection.
Troubleshooting: “My Site Stopped Working After Hardening”
Don’t panic! This usually happens because one of your plugins needs more access than the security settings allow. To identify the problematic plugin:
Disable all plugins
Re-enable them one by one
When you find the one that doesn’t work with the security measures, you can create an exception rule for it
Final Check
After cleaning and hardening, it’s worth performing a quick check to see if you have any unrecognised directories or files that might indicate other compromises. Review your site structure carefully and look for anything unusual or unexpected.
By following these steps, you should be able to recover from this JavaScript hack and better protect your WordPress site against future attacks. Remember that security is an ongoing process—regular updates and monitoring are essential to keeping your site safe.