~mil/mepo

ref: d0fbd028649567c707b0b6cdb34011ac92430999 mepo/src/api/shellpipe.zig -rw-r--r-- 7.4 KiB
d0fbd028Miles Alan Working first pass on async shellpipe; process commands on semicolons 3 months ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
const Mepo = @import("../Mepo.zig");
const types = @import("../types.zig");
const std = @import("std");
const utilconversion = @import("../util/utilconversion.zig");
const utilplatform = @import("../util/utilplatform.zig");
const utildbg = @import("../util/utildbg.zig");
const utilsdl = @import("../util/utilsdl.zig");
const sdl = @import("../sdlshim.zig");

const ShellpipeRequest = struct {
    mepo: *Mepo,
    cmd: []const u8,
};

pub const spec = .{
    .name = "shellpipe",
    .desc = "Run a system (shell) command and pipe; STDOUT returned from command will be executed back as mepolang",
    // TODO: document ENV vars usable in shell scripts context
    .args = (&[_]types.MepoFnSpecArg{
        .{ .tag = .Number, .name = "as_async", .desc = "Set to 1 to run asynchronously (e.g. don't wait for process to complete)" },
        .{ .tag = .Text, .name = "shell_statement", .desc = "Shell statement to execute" },
    })[0..],
    .execute = execute,
};

fn execute(mepo: *Mepo, args: [types.MepoFnNargs]types.MepoArg) !void {
    const as_async = args[0].Number == 1;
    const cmd = args[1].Text;
    try shellpipe(mepo, as_async, cmd);
}

fn shellpipe(mepo: *Mepo, as_async: bool, cmd: []const u8) !void {
    if (as_async) {
        var sp_req = try mepo.allocator.create(ShellpipeRequest);
        sp_req.cmd = try mepo.allocator.dupeZ(u8, cmd);
        sp_req.mepo = mepo;

        _ = sdl.SDL_CreateThread(
            async_shellpipe_run,
            "async_shellpipe",
            sp_req,
        );
    } else {
        try shellpipe_run(mepo, cmd, false);
    }
}

fn async_shellpipe_run(userdata: ?*anyopaque) callconv(.C) c_int {
    var shellpipe_request: *ShellpipeRequest = @ptrCast(*ShellpipeRequest, @alignCast(@alignOf(*ShellpipeRequest), userdata.?));
    // TODO: defer free cmd
    shellpipe_run(shellpipe_request.mepo, shellpipe_request.cmd, true) catch return 1;
    return 0;
}

fn shellpipe_run(mepo: *Mepo, cmd: []const u8, as_async: bool) !void {
    var arena = std.heap.ArenaAllocator.init(mepo.allocator);
    defer arena.deinit();

    mepo.idle_mutex.lock();
    try mepo.update_debug_message(
        try std.fmt.allocPrint(arena.allocator(), "Shellpipe ({s}) in progress - run!", .{cmd}),
    );
    try refresh_ui(mepo, as_async);
    mepo.idle_mutex.unlock();

    if (as_async) {
        // Async:
        // Continually looks at stdout, lock/unlock mutex and run mepolang
        // on every semicolon
        try async_mepolang_execute(mepo, arena.allocator(), cmd);
    } else {
        // Non-async:
        // Basically single lock of the mutex, run result mepolang stdout unlock
        mepo.idle_mutex.lock();
        const env_vars = try get_env_vars(mepo, arena.allocator());
        const args = [_][]const u8{ "sh", "-c", cmd };
        const process_result = try std.ChildProcess.exec(.{
            .allocator = arena.allocator(),
            .argv = args[0..],
            .env_map = &env_vars,
            .max_output_bytes = 10000 * 1024,
        });
        const exitcode = process_result.term.Exited;
        try mepo.update_debug_message(
            try std.fmt.allocPrint(arena.allocator(), "Shellpipe ({s}) completed - execute, {d}!", .{ cmd, process_result.stdout.len }),
        );
        try refresh_ui(mepo, as_async);
        if (exitcode == 0) {
            try mepo.mepolang_execute(process_result.stdout);
            try mepo.update_debug_message(null);
        } else {
            utildbg.log("shellpipe error: exited with code {d}\n{s}", .{ exitcode, process_result.stderr });
            try mepo.update_debug_message(process_result.stderr);
        }
        mepo.idle_mutex.unlock();
    }
}

fn async_mepolang_execute(mepo: *Mepo, allocator: std.mem.Allocator, cmd: []const u8) !void {
    // More or less from zig 0.9 stdlib child process's spawnPosix
    const argv = [_][]const u8{ "sh", "-c", cmd };
    const max_output_bytes: usize = 50 * 1024;
    const child = try std.ChildProcess.init(argv[0..], allocator);
    defer child.deinit();
    child.stdin_behavior = .Ignore;
    child.stdout_behavior = .Pipe;
    try child.spawn();
    var stdout = std.ArrayList(u8).init(allocator);
    errdefer stdout.deinit();

    var poll_fds = [_]std.os.pollfd{
        .{ .fd = child.stdout.?.handle, .events = std.os.POLL.IN, .revents = undefined },
    };
    const bump_amt = 512;

    while (true) loop: {
        const events = try std.os.poll(&poll_fds, std.math.maxInt(i32));
        if (events == 0) continue;
        if (poll_fds[0].revents & std.os.POLL.IN != 0) {
            try stdout.ensureTotalCapacity(std.math.min(stdout.items.len + bump_amt, max_output_bytes));
            if (stdout.unusedCapacitySlice().len == 0) return error.StdoutStreamTooLong;
            const nread = try std.os.read(poll_fds[0].fd, stdout.unusedCapacitySlice());
            if (nread == 0) break :loop;
            stdout.items.len += nread;
            if (std.mem.indexOf(u8, stdout.items, ";")) |mepolang_statement_end_index| {
                const statement = stdout.items[0 .. mepolang_statement_end_index + 1];
                utildbg.log("Running mepolang statement from async shellipe: {s}\n", .{statement});

                mepo.idle_mutex.lock();
                try mepo.mepolang_execute(statement);
                mepo.idle_mutex.unlock();

                // TODO: is there a slice 'shift' like functionality in arraylist?
                var i: usize = mepolang_statement_end_index + 2;
                while (i > 0) : (i -= 1) _ = stdout.swapRemove(0);
            }
        } else if (poll_fds[0].revents & (std.os.POLL.ERR | std.os.POLL.NVAL | std.os.POLL.HUP) != 0) {
            break :loop;
        }
    }
    _ = try child.wait();
}

fn refresh_ui(mepo: *Mepo, as_async: bool) !void {
    if (as_async) utilsdl.sdl_push_resize_event() else try mepo.blit();
}

fn get_env_vars(mepo: *Mepo, allocator: std.mem.Allocator) !std.BufMap {
    var v = try std.process.getEnvMap(allocator);
    const bbox = mepo.bounding_box();

    // TODO: maybe this should be in seperate/helper fn?
    var cursor_x: c_int = undefined;
    var cursor_y: c_int = undefined;
    _ = sdl.SDL_GetMouseState(&cursor_x, &cursor_y);
    const cursor_lat = utilconversion.px_y_to_lat(mepo.get_y() - @divTrunc(@intCast(i32, mepo.win_h), 2) + cursor_y, mepo.zoom);
    const cursor_lon = utilconversion.px_x_to_lon(mepo.get_x() - @divTrunc(@intCast(i32, mepo.win_w), 2) + cursor_x, mepo.zoom);

    // Window and zoom info
    try v.put("MEPO_WIN_W", try std.fmt.allocPrint(allocator, "{d}", .{mepo.win_w}));
    try v.put("MEPO_WIN_H", try std.fmt.allocPrint(allocator, "{d}", .{mepo.win_h}));
    try v.put("MEPO_ZOOM", try std.fmt.allocPrint(allocator, "{d}", .{mepo.zoom}));

    // Center and bbox
    try v.put("MEPO_CENTER_LAT", try std.fmt.allocPrint(allocator, "{d}", .{mepo.lat}));
    try v.put("MEPO_CENTER_LON", try std.fmt.allocPrint(allocator, "{d}", .{mepo.lon}));
    try v.put("MEPO_TL_LAT", try std.fmt.allocPrint(allocator, "{d}", .{bbox.topleft_lat}));
    try v.put("MEPO_TL_LON", try std.fmt.allocPrint(allocator, "{d}", .{bbox.topleft_lon}));
    try v.put("MEPO_BR_LAT", try std.fmt.allocPrint(allocator, "{d}", .{bbox.bottomright_lat}));
    try v.put("MEPO_BR_LON", try std.fmt.allocPrint(allocator, "{d}", .{bbox.bottomright_lon}));

    // Cursor position
    try v.put("MEPO_CURSOR_LAT", try std.fmt.allocPrint(allocator, "{d}", .{cursor_lat}));
    try v.put("MEPO_CURSOR_LON", try std.fmt.allocPrint(allocator, "{d}", .{cursor_lon}));

    return v;
}