diff --git a/.github/workflows/changelog.py b/.github/workflows/changelog.py new file mode 100644 index 0000000..025e054 --- /dev/null +++ b/.github/workflows/changelog.py @@ -0,0 +1,59 @@ +# Note - This is not my original work. +# Credit where credit is due, checkout the original works here: https://github.com/portapack-mayhem/mayhem-firmware/blob/next/.github/workflows/changelog.py +import os +import sys + +import requests +from datetime import datetime, timedelta, timezone + +# Set up your personal access token and the repository details +token = os.environ.get('GH_TOKEN') +repo_owner = "bitlytwiser" +repo_name = "apprunner" + +def print_stable_changelog(): + url = f"compare/{changelog_from_last_commit}...next" + commits = handle_get_request(url) + for commit in commits["commits"]: + # Print the commit details + print(format_output(commit)) + +def changelog_from_last_commit(): + headers = {} if token == None else {"Authorization": f"Bearer {token}"} + url_base = f"https://api.github.com/repos/{repo_owner}/{repo_name}/commits/master" + response = requests.get(url_base, headers=headers) + + if response.status_code == 200: + data = response.json() + return data["sha"] + else: + print(f"Request failed with status code: {response.status_code}") + return None + +def curate_changelog_from_dates(): + # Calculate the date and time 24 hours ago + since_time = (datetime.now(timezone.utc) - timedelta(hours=24)).isoformat() # Check that this is UTC + url = "commits" + commits = handle_get_request(url, since_time) + for commit in commits: + print(format_output(commit)) + +def handle_get_request(path, offset=None): + headers = {} if token == None else {"Authorization": f"Bearer {token}"} + params = {"since": offset} + url_base = f"https://api.github.com/repos/{repo_owner}/{repo_name}/" + response = requests.get(url_base + path, headers=headers, params=params) + + if response.status_code == 200: + return response.json() + else: + print(f"Request failed with status code: {response.status_code}") + return None + + +def format_output(commit): + message_lines = commit["commit"]["message"].split("\n") + author = commit["author"]["login"] if commit["author"] and "login" in commit["author"] else commit["commit"]["author"]["name"] + return '- ' + commit["sha"][:8] + ' - @' + author + ': ' + message_lines[0] + +print_stable_changelog \ No newline at end of file diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..ff700a5 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,123 @@ +name: Binary Release + +on: + push: + branches: + - main + pull_request: + branches: + - main + workflow_dispatch: + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + # Single checkout, with submodules + - name: Checkout + uses: actions/checkout@master + with: + fetch-depth: 0 + submodules: true + + - name: Print latest commit + run: echo ${{ github.sha }} + + - name: "Install Zig" + run: "sudo snap install zig --classic --beta" + + # Date for versioning + - name: Get current date + id: date + run: echo "date=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT + + - name: Get version date + id: version_date + run: echo "date=n_$(date +'%y%m%d')" >> $GITHUB_OUTPUT + + # Linux binary creation + - name: Package and create binary for Linux + run: | + mkdir -p apprunner_linux + zig build -p apprunner_linux/ --release=fast -Dtarget=x86_64-linux + zip -r apprunner_linux.zip apprunner_linux + + # Windows binary creation + - name: Package and create binary for Windows + run: | + mkdir -p apprunner_windows + zig build -p apprunner_windows/--release=fast -Dtarget=x86_64-windows + zip -r apprunner_windows.zip apprunner_windows + + # MacOS Binary Creation + - name: Package and create binary for MacOS + run: | + mkdir -p apprunner_windows + zig build -p apprunner_windows/--release=fast -Dtarget=x86_64-macos + zip -r apprunner_macos.zip apprunner_macos + + # Changelog generation + - name: Create changelog + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + { + echo 'content<> "$GITHUB_OUTPUT" + id: changelog + + # Release creation + - name: Create Release + id: create_release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: binary-tag-${{ steps.date.outputs.date }}-${{ github.sha }} + release_name: Binary Release - ${{ steps.date.outputs.date }} + body: | + **Binary release - ${{ steps.date.outputs.date }}** + This build is the latest code changes for apprunner. + ## Release notes + ### Revision (${{ steps.version_date.outputs.date }}): + ${{ steps.changelog.outputs.content }} + draft: false + prerelease: true + + # Upload binaries (Windows) + - name: Upload Apprunner Binary Windows + id: upload-apprunner-binary-windows + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ./apprunner.zip + asset_name: apprunner${{ steps.version_date.outputs.date }}_binary_windows.zip + asset_content_type: application/zip + + # Upload binaries (Linux) + - name: Upload Apprunner Binary Linux + id: upload-apprunner-binary-linux + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ./apprunner.zip + asset_name: apprunner${{ steps.version_date.outputs.date }}_binary_linux.zip + asset_content_type: application/zip + + # Upload binaries (MacOS) + - name: Upload Apprunner Binary MacOS + id: upload-apprunner-binary-mac + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ./apprunner.zip + asset_name: apprunner${{ steps.version_date.outputs.date }}_binary_macos.zip + asset_content_type: application/zip \ No newline at end of file diff --git a/src/main.zig b/src/main.zig index 238b05a..23097ff 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,6 +1,10 @@ const std = @import("std"); +const builtin = @import("builtin"); +const os = std.os; +const assert = std.debug.assert; const config = @import("./config.zig"); const runner = @import("./runner.zig"); +const shell_err = runner.ShellError; // Functions from std const print = std.debug.print; @@ -23,8 +27,53 @@ pub fn main() !void { defer allocator.free(results.apps); // Spawns all threads and waits - var run = try runner.Runner.init(allocator); + var run = runner.Runner.init(allocator) catch |err| { + switch (err) { + runner.ShellError.ShellNotFound => { + print("error finding appropriate shell to run tmux commands. Please change shells and try again", .{}); + }, + } + return; + }; try run.spawner(results.apps); + + // Listen for the exit events on ctrl+c to gracefully exit + try setAbortSignalHandler(handleAbortSignal); +} + +// Gracefully exit on signal termination events +fn setAbortSignalHandler(comptime handler: *const fn () void) !void { + if (builtin.os.tag == .windows) { + const handler_routine = struct { + fn handler_routine(dwCtrlType: os.windows.DWORD) callconv(os.windows.WINAPI) os.windows.BOOL { + if (dwCtrlType == os.windows.CTRL_C_EVENT) { + handler(); + return os.windows.TRUE; + } else { + return os.windows.FALSE; + } + } + }.handler_routine; + try os.windows.SetConsoleCtrlHandler(handler_routine, true); + } else { + const internal_handler = struct { + fn internal_handler(sig: c_int) callconv(.C) void { + assert(sig == os.linux.SIG.INT); + handler(); + } + }.internal_handler; + const act = os.linux.Sigaction{ + .handler = .{ .handler = internal_handler }, + .mask = os.linux.empty_sigset, + .flags = 0, + }; + _ = os.linux.sigaction(os.linux.SIG.INT, &act, null); + } +} + +fn handleAbortSignal() void { + print("Goodbye! Thanks for using apprunner", .{}); + std.process.exit(0); } test "simple test" {} diff --git a/src/runner.zig b/src/runner.zig index f068b43..789fd3a 100644 --- a/src/runner.zig +++ b/src/runner.zig @@ -1,18 +1,40 @@ const std = @import("std"); +const builtin = @import("builtin"); const config = @import("./config.zig"); const print = std.debug.print; const app_name = "apprunner"; +const default_shell_nix = "bash"; +const default_shell_win = "powershell"; +const cmdline_path = "/proc/$$/cmdline"; + +pub const ShellError = error{ShellNotFound}; + +// Default shells +const shellType = enum { + zsh, + sh, + bash, + powershell, +}; /// Runner is responsible for running all commands parsed from the yaml to TMUX pub const Runner = struct { const Self = @This(); allocator: std.mem.Allocator, + shell_command: []const u8, + shell_sub_command: []const u8, + + pub fn init(allocator: std.mem.Allocator) ShellError!Self { + // Get shell information here so we can exit gracefully + const command_base = commandBase(allocator) catch return ShellError.ShellNotFound; + const sub_command = subCommand() catch return ShellError.ShellNotFound; - pub fn init(allocator: std.mem.Allocator) !Self { return Self{ .allocator = allocator, + .shell_command = command_base, + .shell_sub_command = sub_command, }; } @@ -20,10 +42,13 @@ pub const Runner = struct { var thread_pool = try self.allocator.alloc(std.Thread, apps.len); for (apps, 0..) |app, i| { thread_pool[i] = try std.Thread.spawn(.{ .allocator = self.allocator }, spawnProcess, .{ self, app.name, app.stand, app.command, app.location, i }); + // Its too fast lol - Try sleeping for a moment to avoid missing shells + std.time.sleep(5000 * 3); } // Wait for all threads to stop program from exiting // We could also detach and run forever with a loop + // Ctrl+C event is handled from main for (thread_pool) |thread| { thread.join(); } @@ -40,7 +65,7 @@ pub const Runner = struct { ) !void { // Base command to start tmux session const exec_command = try tmuxConfig(self, name, standalone, command, location, index); - var child = std.process.Child.init(&[_][]const u8{ "sh", "-c", exec_command }, self.allocator); + var child = std.process.Child.init(&[_][]const u8{ self.shell_command, self.shell_sub_command, exec_command }, self.allocator); try child.spawn(); _ = try child.wait(); } @@ -71,3 +96,51 @@ pub const Runner = struct { return r_command; } }; + +/// determines which base command to run depending on execution environment. I.e. windows/linux/macOS +fn commandBase(allocator: std.mem.Allocator) ![]const u8 { + return try captureShell(allocator); +} + +// Process sub-command for running command when spawning shell +fn subCommand() ![]const u8 { + if (builtin.os.tag == .windows) return "-Command"; + + return "-c"; +} + +// In nix systems, parse cmdline_path above to fine current shell. +// Windows its assumed powershell lol +fn captureShell(allocator: std.mem.Allocator) ![]const u8 { + if (builtin.os.tag == .windows) return "powershell"; + + const env_map = try std.process.getEnvMap(allocator); + const shell = env_map.get("SHELL"); + + if (shell) |sh| { + var split_shell = std.mem.splitBackwards(u8, sh, "/"); + + // Shell type is always last + return @as([]const u8, split_shell.first()); + } else { + // Posix systems/macos do not have /proc, so the commands below fail to check the shell + if (builtin.os.tag == .macos) return "bash"; + + const file = try std.fs.openFileAbsolute(cmdline_path, .{ .mode = .read_only }); + defer file.close(); + + // Rather small, but this should only be a single line + var buf: [512]u8 = undefined; + _ = try file.reader().read(&buf); + + return &buf; + } +} + +test "get env variables for shell" { + const env_map = try std.process.getEnvMap(std.heap.page_allocator); + const shell = env_map.get("SHELL") orelse ""; + var split_shell = std.mem.splitBackwards(u8, shell, "/"); + + print("{s}", .{split_shell.first()}); +}