initial commit
29
LICENSE
@@ -201,32 +201,3 @@ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY C
|
|||||||
If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee.
|
If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee.
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
How to Apply These Terms to Your New Programs
|
|
||||||
|
|
||||||
If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms.
|
|
||||||
|
|
||||||
To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the “copyright” line and a pointer to where the full notice is found.
|
|
||||||
|
|
||||||
sparks
|
|
||||||
Copyright (C) 2026 tyler
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
Also add information on how to contact you by electronic and paper mail.
|
|
||||||
|
|
||||||
If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode:
|
|
||||||
|
|
||||||
sparks Copyright (C) 2026 tyler
|
|
||||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
|
||||||
This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details.
|
|
||||||
|
|
||||||
The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an “about box”.
|
|
||||||
|
|
||||||
You should also get your employer (if you work as a programmer) or school, if any, to sign a “copyright disclaimer” for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read <https://www.gnu.org/philosophy/why-not-lgpl.html>.
|
|
||||||
|
|||||||
45
README.md
@@ -1,3 +1,44 @@
|
|||||||
# sparks
|
SPARKS (Spacial Patterns Analysis, Research, & Knowledge Suite)
|
||||||
|
=================================================================
|
||||||
|
|
||||||
Spatial Patterns, Analysis, Research, & Knowledge Suite
|
SPARKS is a standalone application to extract meaningful data out of video files.
|
||||||
|
|
||||||
|
SPARKS is free and open-source software that runs on Windows, MacOS, and Linux. Please read the information regarding each operating system below.
|
||||||
|
|
||||||
|
Visit the official [SPARKS web site](https://research.dezeeuw.ca/sparks).
|
||||||
|
|
||||||
|
[](https://www.python.org)
|
||||||
|
|
||||||
|
# For MacOS Users
|
||||||
|
|
||||||
|
Due to the cost of an Apple Developer account, the application is not certified by Apple. Once the application is extracted and attempted to be launched for the first time you will get a popup stating:
|
||||||
|
|
||||||
|
"Apple could not verify sparks.app is free of malware that may harm your Mac or compromise your privacy.", with the options of "Done" or "Move to Trash".
|
||||||
|
|
||||||
|
The solution around this is to use finder and navigate to the sparks-darwin folder. Once the folder has been located, right click the folder and click the option "New Terminal at Folder". Once the terminal opens, run the following command (you can copy + paste):
|
||||||
|
|
||||||
|
```xattr -dr com.apple.quarantine sparks.app & pid1=$!; xattr -dr com.apple.quarantine sparks_updater.app & pid2=$!; wait $pid1 $pid2; exit```
|
||||||
|
|
||||||
|
Once the command has been executed and the text "[Process completed]" appears, you may close the terminal window and attempt to open the application again. If you choose to unrestrict the app through Settings > Privacy & Security, the app may not be able to update correctly in the future.
|
||||||
|
|
||||||
|
This only applies for the first time you attempt to run SPARKS. Subsequent times, including after updates, will function correctly as-is.
|
||||||
|
|
||||||
|
# For Windows Users
|
||||||
|
|
||||||
|
Due to the cost of a code signing certificate, the application is not digitally signed. Once the application is extracted and attempted to be launched for the first time you will get a popup stating:
|
||||||
|
|
||||||
|
"Windows protected your PC - Microsoft Defender SmartScreen prevented an unrecognized app from starting. Running this app might put your PC at risk.", with the options of" More info" or "Don't run".
|
||||||
|
|
||||||
|
The solution around this is to click "More info" and then select "Run anyway".
|
||||||
|
|
||||||
|
This only applies for the first time you attempt to run SPARKS. Subsequent times, including after updates, will function correctly as-is.
|
||||||
|
|
||||||
|
# For Linux Users
|
||||||
|
|
||||||
|
There are no conditions for Linux users at this time.
|
||||||
|
|
||||||
|
# Licence
|
||||||
|
|
||||||
|
SPARKS is distributed under the GPL-3.0 license.
|
||||||
|
|
||||||
|
Copyright (C) 2025-2026 Tyler de Zeeuw
|
||||||
3
changelog.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Version 0.1.0
|
||||||
|
|
||||||
|
- Initial preview release.
|
||||||
1
icons/article_24dp_1F1F1F.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#1f1f1f"><path d="M0 0h24v24H0z" fill="none"/><path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-5 14H7v-2h7v2zm3-4H7v-2h10v2zm0-4H7V7h10v2z"/></svg>
|
||||||
|
After Width: | Height: | Size: 278 B |
1
icons/content_copy_24dp_1F1F1F.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#1f1f1f"><path d="M0 0h24v24H0z" fill="none"/><path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/></svg>
|
||||||
|
After Width: | Height: | Size: 284 B |
1
icons/content_cut_24dp_1F1F1F.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#1f1f1f"><path d="M0 0h24v24H0z" fill="none"/><circle cx="6" cy="18" fill="none" r="2"/><circle cx="12" cy="12" fill="none" r=".5"/><circle cx="6" cy="6" fill="none" r="2"/><path d="M9.64 7.64c.23-.5.36-1.05.36-1.64 0-2.21-1.79-4-4-4S2 3.79 2 6s1.79 4 4 4c.59 0 1.14-.13 1.64-.36L10 12l-2.36 2.36C7.14 14.13 6.59 14 6 14c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4c0-.59-.13-1.14-.36-1.64L12 14l7 7h3v-1L9.64 7.64zM6 8c-1.1 0-2-.89-2-2s.9-2 2-2 2 .89 2 2-.9 2-2 2zm0 12c-1.1 0-2-.89-2-2s.9-2 2-2 2 .89 2 2-.9 2-2 2zm6-7.5c-.28 0-.5-.22-.5-.5s.22-.5.5-.5.5.22.5.5-.22.5-.5.5zM19 3l-6 6 2 2 7-7V3z"/></svg>
|
||||||
|
After Width: | Height: | Size: 694 B |
1
icons/content_paste_24dp_1F1F1F.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#1f1f1f"><path d="M0 0h24v24H0z" fill="none"/><path d="M19 2h-4.18C14.4.84 13.3 0 12 0c-1.3 0-2.4.84-2.82 2H5c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-7 0c.55 0 1 .45 1 1s-.45 1-1 1-1-.45-1-1 .45-1 1-1zm7 18H5V4h2v3h10V4h2v16z"/></svg>
|
||||||
|
After Width: | Height: | Size: 357 B |
1
icons/exit_to_app_24dp_1F1F1F.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#1f1f1f"><path d="M0 0h24v24H0z" fill="none"/><path d="M10.09 15.59L11.5 17l5-5-5-5-1.41 1.41L12.67 11H3v2h9.67l-2.58 2.59zM19 3H5c-1.11 0-2 .9-2 2v4h2V5h14v14H5v-4H3v4c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z"/></svg>
|
||||||
|
After Width: | Height: | Size: 321 B |
1
icons/file_open_24dp_1F1F1F.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24px" viewBox="0 0 24 24" width="24px" fill="#1f1f1f"><g><rect fill="none" height="24" width="24"/></g><g><path d="M14,2H6C4.9,2,4,2.9,4,4v16c0,1.1,0.89,2,1.99,2H15v-8h5V8L14,2z M13,9V3.5L18.5,9H13z M17,21.66V16h5.66v2h-2.24 l2.95,2.95l-1.41,1.41L19,19.41l0,2.24H17z"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 361 B |
1
icons/folder_24dp_1F1F1F.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#1f1f1f"><path d="M0 0h24v24H0z" fill="none"/><path d="M10 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/></svg>
|
||||||
|
After Width: | Height: | Size: 248 B |
1
icons/folder_copy_24dp_1F1F1F.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24px" viewBox="0 0 24 24" width="24px" fill="#1f1f1f"><g><rect fill="none" height="24" width="24"/></g><g><g><path d="M3,6H1v13c0,1.1,0.9,2,2,2h17v-2H3V6z"/><path d="M21,4h-7l-2-2H7C5.9,2,5.01,2.9,5.01,4L5,15c0,1.1,0.9,2,2,2h14c1.1,0,2-0.9,2-2V6C23,4.9,22.1,4,21,4z"/></g></g></svg>
|
||||||
|
After Width: | Height: | Size: 364 B |
1
icons/folder_eye_24dp_1F1F1F.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1f1f1f"><path d="M160-160q-33 0-56.5-23.5T80-240v-480q0-33 23.5-56.5T160-800h240l80 80h320q33 0 56.5 23.5T880-640v242q-18-14-38-23t-42-19v-200H447l-80-80H160v480h120v80H160ZM640-40q-91 0-168-48T360-220q35-84 112-132t168-48q91 0 168 48t112 132q-35 84-112 132T640-40Zm0-80q57 0 107.5-26t82.5-74q-32-48-82.5-74T640-320q-57 0-107.5 26T450-220q32 48 82.5 74T640-120Zm0-40q-25 0-42.5-17.5T580-220q0-25 17.5-42.5T640-280q25 0 42.5 17.5T700-220q0 25-17.5 42.5T640-160Zm-480-80v-480 277-37 240Z"/></svg>
|
||||||
|
After Width: | Height: | Size: 593 B |
1
icons/help_24dp_1F1F1F.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#1f1f1f"><path d="M0 0h24v24H0z" fill="none"/><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 17h-2v-2h2v2zm2.07-7.75l-.9.92C13.45 12.9 13 13.5 13 15h-2v-.5c0-1.1.45-2.1 1.17-2.83l1.24-1.26c.37-.36.59-.86.59-1.41 0-1.1-.9-2-2-2s-2 .9-2 2H8c0-2.21 1.79-4 4-4s4 1.79 4 4c0 .88-.36 1.68-.93 2.25z"/></svg>
|
||||||
|
After Width: | Height: | Size: 425 B |
1
icons/info_24dp_1F1F1F.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#1f1f1f"><path d="M0 0h24v24H0z" fill="none"/><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z"/></svg>
|
||||||
|
After Width: | Height: | Size: 253 B |
BIN
icons/main.icns
Normal file
BIN
icons/main.ico
Normal file
|
After Width: | Height: | Size: 192 KiB |
1
icons/remove_24dp_1F1F1F.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1f1f1f"><path d="M200-440v-80h560v80H200Z"/></svg>
|
||||||
|
After Width: | Height: | Size: 149 B |
1
icons/save_24dp_1F1F1F.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#1f1f1f"><path d="M0 0h24v24H0z" fill="none"/><path d="M17 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V7l-4-4zm-5 16c-1.66 0-3-1.34-3-3s1.34-3 3-3 3 1.34 3 3-1.34 3-3 3zm3-10H5V5h10v4z"/></svg>
|
||||||
|
After Width: | Height: | Size: 299 B |
1
icons/save_as_24dp_1F1F1F.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24px" viewBox="0 0 24 24" width="24px" fill="#1f1f1f"><g><rect fill="none" height="24" width="24"/></g><g><path d="M21,12.4V7l-4-4H5C3.89,3,3,3.9,3,5v14c0,1.1,0.89,2,2,2h7.4L21,12.4z M15,15c0,1.66-1.34,3-3,3s-3-1.34-3-3s1.34-3,3-3 S15,13.34,15,15z M6,6h9v4H6V6z M19.99,16.25l1.77,1.77L16.77,23H15v-1.77L19.99,16.25z M23.25,16.51l-0.85,0.85l-1.77-1.77 l0.85-0.85c0.2-0.2,0.51-0.2,0.71,0l1.06,1.06C23.45,16,23.45,16.32,23.25,16.51z"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 524 B |
1
icons/terminal_24dp_1F1F1F.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1f1f1f"><path d="M160-160q-33 0-56.5-23.5T80-240v-480q0-33 23.5-56.5T160-800h640q33 0 56.5 23.5T880-720v480q0 33-23.5 56.5T800-160H160Zm0-80h640v-400H160v400Zm140-40-56-56 103-104-104-104 57-56 160 160-160 160Zm180 0v-80h240v80H480Z"/></svg>
|
||||||
|
After Width: | Height: | Size: 340 B |
1
icons/update_24dp_1F1F1F.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24px" viewBox="0 0 24 24" width="24px" fill="#1f1f1f"><g><rect fill="none" height="24" width="24" x="0"/></g><g><g><g><path d="M21,10.12h-6.78l2.74-2.82c-2.73-2.7-7.15-2.8-9.88-0.1c-2.73,2.71-2.73,7.08,0,9.79s7.15,2.71,9.88,0 C18.32,15.65,19,14.08,19,12.1h2c0,1.98-0.88,4.55-2.64,6.29c-3.51,3.48-9.21,3.48-12.72,0c-3.5-3.47-3.53-9.11-0.02-12.58 s9.14-3.47,12.65,0L21,3V10.12z M12.5,8v4.25l3.5,2.08l-0.72,1.21L11,13V8H12.5z"/></g></g></g></svg>
|
||||||
|
After Width: | Height: | Size: 525 B |
BIN
icons/updater.icns
Normal file
BIN
icons/updater.ico
Normal file
|
After Width: | Height: | Size: 186 KiB |
1
icons/upgrade_24dp_1F1F1F.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1f1f1f"><path d="M280-160v-80h400v80H280Zm160-160v-327L336-544l-56-56 200-200 200 200-56 56-104-103v327h-80Z"/></svg>
|
||||||
|
After Width: | Height: | Size: 216 B |
255
sparks_updater.py
Normal file
@@ -0,0 +1,255 @@
|
|||||||
|
"""
|
||||||
|
Filename: sparks_updater.py
|
||||||
|
Description: SPARKS updater executable
|
||||||
|
|
||||||
|
Author: Tyler de Zeeuw
|
||||||
|
License: GPL-3.0
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Built-in imports
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import shlex
|
||||||
|
import psutil
|
||||||
|
import shutil
|
||||||
|
import platform
|
||||||
|
import subprocess
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
PLATFORM_NAME = platform.system().lower()
|
||||||
|
|
||||||
|
if PLATFORM_NAME == 'darwin':
|
||||||
|
LOG_FILE = os.path.join(os.path.dirname(sys.executable), "../../../sparks_updater.log")
|
||||||
|
else:
|
||||||
|
LOG_FILE = os.path.join(os.getcwd(), "sparks_updater.log")
|
||||||
|
|
||||||
|
|
||||||
|
def log(msg):
|
||||||
|
with open(LOG_FILE, "a", encoding="utf-8") as f:
|
||||||
|
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
f.write(f"{timestamp} - {msg}\n")
|
||||||
|
|
||||||
|
|
||||||
|
def kill_all_processes_by_executable(exe_path):
|
||||||
|
terminated_any = False
|
||||||
|
exe_path = os.path.realpath(exe_path)
|
||||||
|
|
||||||
|
if PLATFORM_NAME == 'windows':
|
||||||
|
for proc in psutil.process_iter(['pid', 'exe']):
|
||||||
|
try:
|
||||||
|
proc_exe = proc.info.get('exe')
|
||||||
|
if proc_exe and os.path.samefile(os.path.realpath(proc_exe), exe_path):
|
||||||
|
log(f"Terminating process: PID {proc.pid}")
|
||||||
|
_terminate_process(proc)
|
||||||
|
terminated_any = True
|
||||||
|
except Exception as e:
|
||||||
|
log(f"Error terminating process (Windows): {e}")
|
||||||
|
elif PLATFORM_NAME == 'linux':
|
||||||
|
for proc in psutil.process_iter(['pid', 'cmdline']):
|
||||||
|
try:
|
||||||
|
cmdline = proc.info.get('cmdline', [])
|
||||||
|
if cmdline:
|
||||||
|
proc_cmd = os.path.realpath(cmdline[0])
|
||||||
|
if os.path.samefile(proc_cmd, exe_path):
|
||||||
|
log(f"Terminating process: PID {proc.pid}")
|
||||||
|
_terminate_process(proc)
|
||||||
|
terminated_any = True
|
||||||
|
except Exception as e:
|
||||||
|
log(f"Error terminating process (Linux): {e}")
|
||||||
|
|
||||||
|
if not terminated_any:
|
||||||
|
log(f"No running processes found for {exe_path}")
|
||||||
|
return terminated_any
|
||||||
|
|
||||||
|
|
||||||
|
def _terminate_process(proc):
|
||||||
|
try:
|
||||||
|
proc.terminate()
|
||||||
|
proc.wait(timeout=10)
|
||||||
|
log(f"Process {proc.pid} terminated gracefully.")
|
||||||
|
except psutil.TimeoutExpired:
|
||||||
|
log(f"Process {proc.pid} did not terminate in time. Killing forcefully.")
|
||||||
|
proc.kill()
|
||||||
|
proc.wait(timeout=5)
|
||||||
|
log(f"Process {proc.pid} killed.")
|
||||||
|
|
||||||
|
|
||||||
|
def wait_for_unlock(path, timeout=100):
|
||||||
|
start_time = time.time()
|
||||||
|
while time.time() - start_time < timeout:
|
||||||
|
try:
|
||||||
|
if os.path.isdir(path):
|
||||||
|
shutil.rmtree(path)
|
||||||
|
else:
|
||||||
|
os.remove(path)
|
||||||
|
log(f"Deleted (after wait): {path}")
|
||||||
|
return
|
||||||
|
except Exception as e:
|
||||||
|
log(f"Still locked: {path} - {e}")
|
||||||
|
time.sleep(1)
|
||||||
|
log(f"Failed to delete after wait: {path}")
|
||||||
|
|
||||||
|
|
||||||
|
def delete_path(path):
|
||||||
|
if os.path.exists(path):
|
||||||
|
try:
|
||||||
|
if os.path.isdir(path):
|
||||||
|
shutil.rmtree(path)
|
||||||
|
log(f"Deleted directory: {path}")
|
||||||
|
else:
|
||||||
|
os.remove(path)
|
||||||
|
log(f"Deleted file: {path}")
|
||||||
|
except Exception as e:
|
||||||
|
log(f"Error deleting {path}: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def copy_update_files(src_folder, dest_folder, updater_name):
|
||||||
|
for item in os.listdir(src_folder):
|
||||||
|
if item.lower() == updater_name.lower():
|
||||||
|
log(f"Skipping updater executable: {item}")
|
||||||
|
continue
|
||||||
|
s = os.path.join(src_folder, item)
|
||||||
|
d = os.path.join(dest_folder, item)
|
||||||
|
delete_path(d)
|
||||||
|
try:
|
||||||
|
if os.path.isdir(s):
|
||||||
|
shutil.copytree(s, d)
|
||||||
|
log(f"Copied folder: {s} -> {d}")
|
||||||
|
else:
|
||||||
|
shutil.copy2(s, d)
|
||||||
|
log(f"Copied file: {s} -> {d}")
|
||||||
|
except Exception as e:
|
||||||
|
log(f"Error copying {s} -> {d}: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def copy_update_files_darwin(src_folder, dest_folder, updater_name):
|
||||||
|
|
||||||
|
updater_name = updater_name + ".app"
|
||||||
|
|
||||||
|
for item in os.listdir(src_folder):
|
||||||
|
if item.lower() == updater_name.lower():
|
||||||
|
log(f"Skipping updater executable: {item}")
|
||||||
|
continue
|
||||||
|
s = os.path.join(src_folder, item)
|
||||||
|
d = os.path.join(dest_folder, item)
|
||||||
|
delete_path(d)
|
||||||
|
try:
|
||||||
|
if os.path.isdir(s):
|
||||||
|
subprocess.check_call(["ditto", s, d])
|
||||||
|
log(f"Copied folder with ditto: {s} -> {d}")
|
||||||
|
else:
|
||||||
|
shutil.copy2(s, d)
|
||||||
|
log(f"Copied file: {s} -> {d}")
|
||||||
|
except Exception as e:
|
||||||
|
log(f"Error copying {s} -> {d}: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def remove_quarantine(app_path):
|
||||||
|
script = f'''
|
||||||
|
do shell script "xattr -d -r com.apple.quarantine {shlex.quote(app_path)}" with administrator privileges with prompt "SPARKS needs privileges to finish the update. (1/2)"
|
||||||
|
'''
|
||||||
|
try:
|
||||||
|
subprocess.run(['osascript', '-e', script], check=True)
|
||||||
|
print("✅ Quarantine attribute removed.")
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
print("❌ Failed to remove quarantine attribute.")
|
||||||
|
print(e)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
try:
|
||||||
|
log(f"[Updater] sys.argv: {sys.argv}")
|
||||||
|
|
||||||
|
if len(sys.argv) != 3:
|
||||||
|
log("Invalid arguments. Usage: sparks_updater <update_folder> <main_app_executable>")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
update_folder = sys.argv[1]
|
||||||
|
main_exe = sys.argv[2]
|
||||||
|
|
||||||
|
# Interesting naming convention
|
||||||
|
parent_dir = os.path.dirname(os.path.abspath(main_exe))
|
||||||
|
pparent_dir = os.path.dirname(parent_dir)
|
||||||
|
ppparent_dir = os.path.dirname(pparent_dir)
|
||||||
|
pppparent_dir = os.path.dirname(ppparent_dir)
|
||||||
|
|
||||||
|
updater_name = os.path.basename(sys.argv[0])
|
||||||
|
|
||||||
|
log("Updater started.")
|
||||||
|
log(f"Update folder: {update_folder}")
|
||||||
|
log(f"Main EXE: {main_exe}")
|
||||||
|
log(f"Updater EXE: {updater_name}")
|
||||||
|
if PLATFORM_NAME == 'darwin':
|
||||||
|
log(f"Main App Folder: {ppparent_dir}")
|
||||||
|
|
||||||
|
# Kill all instances of main app
|
||||||
|
kill_all_processes_by_executable(main_exe)
|
||||||
|
|
||||||
|
# Wait until main_exe process is fully gone (polling)
|
||||||
|
for _ in range(20): # wait max 10 seconds
|
||||||
|
running = False
|
||||||
|
for proc in psutil.process_iter(['exe', 'cmdline']):
|
||||||
|
try:
|
||||||
|
if PLATFORM_NAME == 'windows':
|
||||||
|
proc_exe = proc.info.get('exe')
|
||||||
|
if proc_exe and os.path.samefile(os.path.realpath(proc_exe), os.path.realpath(main_exe)):
|
||||||
|
running = True
|
||||||
|
break
|
||||||
|
elif PLATFORM_NAME == 'linux':
|
||||||
|
cmdline = proc.info.get('cmdline', [])
|
||||||
|
if cmdline:
|
||||||
|
proc_cmd = os.path.realpath(cmdline[0])
|
||||||
|
if os.path.samefile(proc_cmd, os.path.realpath(main_exe)):
|
||||||
|
running = True
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
log(f"Polling error: {e}")
|
||||||
|
if not running:
|
||||||
|
break
|
||||||
|
time.sleep(0.5)
|
||||||
|
else:
|
||||||
|
log("Warning: main executable still running after wait timeout.")
|
||||||
|
|
||||||
|
# Delete old version files
|
||||||
|
if PLATFORM_NAME == 'darwin':
|
||||||
|
log(f'Attempting to delete {ppparent_dir}')
|
||||||
|
delete_path(ppparent_dir)
|
||||||
|
update_folder = os.path.join(sys.argv[1], "sparks-darwin")
|
||||||
|
copy_update_files_darwin(update_folder, pppparent_dir, updater_name)
|
||||||
|
|
||||||
|
else:
|
||||||
|
delete_path(main_exe)
|
||||||
|
wait_for_unlock(os.path.join(parent_dir, "_internal"))
|
||||||
|
|
||||||
|
# Copy new files excluding the updater itself
|
||||||
|
copy_update_files(update_folder, parent_dir, updater_name)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
log(f"Something went wrong: {e}")
|
||||||
|
|
||||||
|
# Relaunch main app
|
||||||
|
try:
|
||||||
|
if PLATFORM_NAME == 'linux':
|
||||||
|
os.chmod(main_exe, 0o755)
|
||||||
|
log("Added executable bit")
|
||||||
|
|
||||||
|
if PLATFORM_NAME == 'darwin':
|
||||||
|
os.chmod(ppparent_dir, 0o755)
|
||||||
|
log("Added executable bit")
|
||||||
|
remove_quarantine(ppparent_dir)
|
||||||
|
log(f"Removed the quarantine flag on {ppparent_dir}")
|
||||||
|
subprocess.Popen(['open', ppparent_dir, "--args", "--finish-update"])
|
||||||
|
else:
|
||||||
|
subprocess.Popen([main_exe, "--finish-update"], cwd=parent_dir)
|
||||||
|
|
||||||
|
log("Relaunched main app.")
|
||||||
|
except Exception as e:
|
||||||
|
log(f"Failed to relaunch main app: {e}")
|
||||||
|
|
||||||
|
log("Updater completed. Exiting.")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
29
version_main.txt
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
VSVersionInfo(
|
||||||
|
ffi=FixedFileInfo(
|
||||||
|
filevers=(1, 0, 0, 0),
|
||||||
|
prodvers=(1, 0, 0, 0),
|
||||||
|
mask=0x3f,
|
||||||
|
flags=0x0,
|
||||||
|
OS=0x4,
|
||||||
|
fileType=0x1,
|
||||||
|
subtype=0x0,
|
||||||
|
date=(0, 0)
|
||||||
|
),
|
||||||
|
kids=[
|
||||||
|
StringFileInfo(
|
||||||
|
[
|
||||||
|
StringTable(
|
||||||
|
'040904B0',
|
||||||
|
[StringStruct('CompanyName', 'Tyler de Zeeuw'),
|
||||||
|
StringStruct('FileDescription', 'SPARKS main application'),
|
||||||
|
StringStruct('FileVersion', '1.0.0.0'),
|
||||||
|
StringStruct('InternalName', 'sparks.exe'),
|
||||||
|
StringStruct('LegalCopyright', '© 2025 Tyler de Zeeuw'),
|
||||||
|
StringStruct('OriginalFilename', 'sparks.exe'),
|
||||||
|
StringStruct('ProductName', 'SPARKS'),
|
||||||
|
StringStruct('ProductVersion', '1.0.0.0')])
|
||||||
|
]
|
||||||
|
),
|
||||||
|
VarFileInfo([VarStruct('Translation', [1033, 1200])])
|
||||||
|
]
|
||||||
|
)
|
||||||
29
version_updater.txt
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
VSVersionInfo(
|
||||||
|
ffi=FixedFileInfo(
|
||||||
|
filevers=(1, 0, 0, 0),
|
||||||
|
prodvers=(1, 0, 0, 0),
|
||||||
|
mask=0x3f,
|
||||||
|
flags=0x0,
|
||||||
|
OS=0x4,
|
||||||
|
fileType=0x1,
|
||||||
|
subtype=0x0,
|
||||||
|
date=(0, 0)
|
||||||
|
),
|
||||||
|
kids=[
|
||||||
|
StringFileInfo(
|
||||||
|
[
|
||||||
|
StringTable(
|
||||||
|
'040904B0',
|
||||||
|
[StringStruct('CompanyName', 'Tyler de Zeeuw'),
|
||||||
|
StringStruct('FileDescription', 'SPARKS updater application'),
|
||||||
|
StringStruct('FileVersion', '1.0.0.0'),
|
||||||
|
StringStruct('InternalName', 'main.exe'),
|
||||||
|
StringStruct('LegalCopyright', '© 2025 Tyler de Zeeuw'),
|
||||||
|
StringStruct('OriginalFilename', 'sparks_updater.exe'),
|
||||||
|
StringStruct('ProductName', 'SPARKS Updater'),
|
||||||
|
StringStruct('ProductVersion', '1.0.0.0')])
|
||||||
|
]
|
||||||
|
),
|
||||||
|
VarFileInfo([VarStruct('Translation', [1033, 1200])])
|
||||||
|
]
|
||||||
|
)
|
||||||