╭ try.zig ──────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
1 //! start a server with some fake data and then launch 2 //! the TUI. this provides a nice way to test things 3 //! out safely. 4 5 const std = @import("std"); 6 const builtin = @import("builtin"); 7 const hx = @import("haxy"); 8 const srv = hx.serve; 9 const evt = hx.event; 10 const xit = hx.xit; 11 const rp = xit.repo; 12 const ui = hx.ui; 13 14 pub fn main(init: std.process.Init) !void { 15 var gpa: std.heap.DebugAllocator(.{}) = .init; 16 defer _ = gpa.deinit(); 17 const allocator = gpa.allocator(); 18 19 var threaded = std.Io.Threaded.init(allocator, .{ 20 .environ = init.minimal.environ, 21 }); 22 defer threaded.deinit(); 23 const io = threaded.io(); 24 25 const temp_dir_name = "temp-try"; 26 27 // create the temp dir 28 const cwd = std.Io.Dir.cwd(); 29 var temp_dir_or_err = cwd.openDir(io, temp_dir_name, .{}); 30 if (temp_dir_or_err) |*temp_dir| { 31 temp_dir.close(io); 32 try cwd.deleteTree(io, temp_dir_name); 33 } else |_| {} 34 var temp_dir = try cwd.createDirPathOpen(io, temp_dir_name, .{}); 35 defer cwd.deleteTree(io, temp_dir_name) catch {}; 36 defer temp_dir.close(io); 37 38 const cwd_path = try std.process.currentPathAlloc(io, allocator); 39 defer allocator.free(cwd_path); 40 41 var page_arena = std.heap.ArenaAllocator.init(allocator); 42 defer page_arena.deinit(); 43 44 // create the admin repo and build the Page from it 45 const page: ui.Page = blk: { 46 const work_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "server", "admin" }); 47 defer allocator.free(work_path); 48 49 const repo_opts: rp.RepoOpts(.xit) = .{ .is_test = true }; 50 const Repo = rp.Repo(.xit, repo_opts); 51 var repo = try Repo.init(io, allocator, .{ .path = work_path }); 52 defer repo.deinit(io, allocator); 53 54 var arena = std.heap.ArenaAllocator.init(allocator); 55 defer arena.deinit(); 56 57 // define test events 58 59 var prng = std.Random.DefaultPrng.init(std.testing.random_seed); 60 61 const user_data = [_]struct { 62 name: []const u8, 63 display_name: []const u8, 64 email: []const u8, 65 }{ 66 .{ .name = "alice", .display_name = "Alice Tulley", .email = "alice@example.test" }, 67 .{ .name = "bob", .display_name = "Bob Smith", .email = "bob@example.test" }, 68 .{ .name = "carol", .display_name = "Carol Johnson", .email = "carol@example.test" }, 69 .{ .name = "dave", .display_name = "Dave Wilson", .email = "dave@example.test" }, 70 .{ .name = "eve", .display_name = "Eve Anderson", .email = "eve@example.test" }, 71 .{ .name = "frank", .display_name = "Frank Miller", .email = "frank@example.test" }, 72 .{ .name = "grace", .display_name = "Grace Lee", .email = "grace@example.test" }, 73 .{ .name = "henry", .display_name = "Henry Davis", .email = "henry@example.test" }, 74 .{ .name = "ivy", .display_name = "Ivy Martinez", .email = "ivy@example.test" }, 75 .{ .name = "jack", .display_name = "Jack Thompson", .email = "jack@example.test" }, 76 .{ .name = "kate", .display_name = "Kate Robinson", .email = "kate@example.test" }, 77 .{ .name = "liam", .display_name = "Liam Walker", .email = "liam@example.test" }, 78 .{ .name = "mona", .display_name = "Mona Patel", .email = "mona@example.test" }, 79 .{ .name = "noah", .display_name = "Noah Garcia", .email = "noah@example.test" }, 80 .{ .name = "olivia", .display_name = "Olivia Hernandez", .email = "olivia@example.test" }, 81 .{ .name = "peter", .display_name = "Peter Wright", .email = "peter@example.test" }, 82 .{ .name = "quinn", .display_name = "Quinn Foster", .email = "quinn@example.test" }, 83 .{ .name = "rachel", .display_name = "Rachel Bennett", .email = "rachel@example.test" }, 84 .{ .name = "sam", .display_name = "Sam Brooks", .email = "sam@example.test" }, 85 .{ .name = "tina", .display_name = "Tina Cooper", .email = "tina@example.test" }, 86 }; 87 88 const repo_data = [_]struct { 89 user_index: usize, 90 name: []const u8, 91 description: []const u8, 92 }{ 93 .{ .user_index = 0, .name = "ziglings", .description = "Learn the Zig programming language by fixing tiny broken programs" }, 94 .{ .user_index = 1, .name = "linux", .description = "Linux kernel source tree" }, 95 .{ .user_index = 2, .name = "kubernetes", .description = "Production-grade container orchestration" }, 96 .{ .user_index = 3, .name = "react", .description = "A declarative, efficient, and flexible JavaScript library for building user interfaces" }, 97 .{ .user_index = 4, .name = "typescript", .description = "TypeScript is a superset of JavaScript that compiles to clean JavaScript output" }, 98 .{ .user_index = 5, .name = "rust", .description = "Empowering everyone to build reliable and efficient software" }, 99 .{ .user_index = 6, .name = "go", .description = "The Go programming language" }, 100 .{ .user_index = 7, .name = "nodejs", .description = "Node.js JavaScript runtime" }, 101 .{ .user_index = 8, .name = "cpython", .description = "The Python programming language" }, 102 .{ .user_index = 9, .name = "docker", .description = "Container platform for developing, shipping, and running applications" }, 103 .{ .user_index = 0, .name = "vim", .description = "The ubiquitous text editor" }, 104 .{ .user_index = 1, .name = "neovim", .description = "Hyperextensible Vim-based text editor" }, 105 .{ .user_index = 2, .name = "emacs", .description = "GNU Emacs source code mirror" }, 106 .{ .user_index = 3, .name = "tmux", .description = "Terminal multiplexer" }, 107 .{ .user_index = 4, .name = "zsh", .description = "Mirror of the Z shell source code repository" }, 108 .{ .user_index = 5, .name = "git", .description = "Distributed version control system" }, 109 .{ .user_index = 6, .name = "mercurial", .description = "Source-control management tool" }, 110 .{ .user_index = 7, .name = "tensorflow", .description = "An end-to-end open source machine learning platform" }, 111 .{ .user_index = 8, .name = "pytorch", .description = "Tensors and dynamic neural networks in Python with strong GPU acceleration" }, 112 .{ .user_index = 9, .name = "numpy", .description = "The fundamental package for scientific computing with Python" }, 113 .{ .user_index = 0, .name = "pandas", .description = "Flexible and powerful data analysis and manipulation library for Python" }, 114 .{ .user_index = 1, .name = "scikit-learn", .description = "Machine learning in Python" }, 115 .{ .user_index = 2, .name = "nginx", .description = "High performance HTTP server and reverse proxy" }, 116 .{ .user_index = 3, .name = "redis", .description = "In-memory data structure store, used as a database, cache, and message broker" }, 117 .{ .user_index = 4, .name = "postgres", .description = "The world's most advanced open source relational database" }, 118 .{ .user_index = 5, .name = "sqlite", .description = "Self-contained, serverless, zero-configuration SQL database engine" }, 119 .{ .user_index = 6, .name = "mongodb", .description = "The MongoDB Database" }, 120 .{ .user_index = 7, .name = "elasticsearch", .description = "Free and open, distributed, RESTful search engine" }, 121 .{ .user_index = 8, .name = "kafka", .description = "Distributed event streaming platform" }, 122 .{ .user_index = 9, .name = "terraform", .description = "Infrastructure as code tool" }, 123 .{ .user_index = 10, .name = "svelte", .description = "Cybernetically enhanced web apps" }, 124 .{ .user_index = 11, .name = "vue", .description = "The progressive JavaScript framework" }, 125 .{ .user_index = 12, .name = "flask", .description = "The Python micro framework for building web applications" }, 126 .{ .user_index = 13, .name = "django", .description = "The web framework for perfectionists with deadlines" }, 127 .{ .user_index = 14, .name = "rails", .description = "Ruby on Rails web framework" }, 128 .{ .user_index = 15, .name = "phoenix", .description = "Peace of mind from prototype to production for Elixir web apps" }, 129 .{ .user_index = 16, .name = "laravel", .description = "The PHP framework for web artisans" }, 130 .{ .user_index = 17, .name = "prometheus", .description = "The Prometheus monitoring system and time series database" }, 131 .{ .user_index = 18, .name = "grafana", .description = "The open and composable observability and data visualization platform" }, 132 .{ .user_index = 19, .name = "ansible", .description = "Simple, agentless IT automation" }, 133 }; 134 135 var user_ids: [user_data.len][evt.event_id_size]u8 = undefined; 136 for (&user_ids) |*id| id.* = evt.EventWithId.randomId(prng.random()); 137 138 var repo_event_ids: [repo_data.len][evt.event_id_size]u8 = undefined; 139 for (&repo_event_ids) |*id| id.* = evt.EventWithId.randomId(prng.random()); 140 141 var password_hash_buf: [evt.User.password_hash_max_len]u8 = undefined; 142 const password_hash = try evt.User.hashPassword("correct horse battery staple", &password_hash_buf, io); 143 144 var events_to_consume: [user_data.len + repo_data.len]evt.EventWithId = undefined; 145 for (user_data, 0..) |u, i| { 146 events_to_consume[i] = .{ 147 .id = std.fmt.bytesToHex(user_ids[i], .lower), 148 .event = .{ 149 .user = .{ 150 .name = u.name, 151 .display_name = u.display_name, 152 .email = u.email, 153 .password_hash = password_hash, 154 }, 155 }, 156 }; 157 } 158 for (repo_data, 0..) |r, i| { 159 events_to_consume[user_data.len + i] = .{ 160 .id = std.fmt.bytesToHex(repo_event_ids[i], .lower), 161 .event = .{ 162 .repo = .{ 163 .user_id = &user_ids[r.user_index], 164 .name = r.name, 165 .description = r.description, 166 .enable_issue = true, 167 }, 168 }, 169 }; 170 } 171 172 // insert users and repos as commits in the repo 173 { 174 var json: std.Io.Writer.Allocating = .init(allocator); 175 defer json.deinit(); 176 177 for (events_to_consume) |event| { 178 json.clearRetainingCapacity(); 179 180 try std.json.Stringify.value(event, .{}, &json.writer); 181 182 // commit the event into a special branch 183 _ = try repo.commitAtRef(io, allocator, .{ .message = json.written() }, null, .{ .kind = .head, .name = "haxy/meta" }); 184 } 185 } 186 187 // consume events into the database 188 try evt.consume(repo_opts, io, allocator, &repo, .{ .kind = .head, .name = "haxy/meta" }); 189 190 break :blk .{ .users_and_repos = try .init(repo_opts, &page_arena, &repo) }; 191 }; 192 193 // start the server 194 195 var launch_sshd = false; 196 197 var arg_it = try init.minimal.args.iterateAllocator(allocator); 198 defer arg_it.deinit(); 199 _ = arg_it.skip(); 200 while (arg_it.next()) |arg| { 201 if (std.mem.eql(u8, "--sshd", arg)) { 202 launch_sshd = true; 203 } 204 } 205 206 const server_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "server" }); 207 defer allocator.free(server_path); 208 209 if (launch_sshd) { 210 var stdout_writer = std.Io.File.stdout().writer(io, &.{}); 211 var stderr_writer = std.Io.File.stderr().writer(io, &.{}); 212 const run_opts = hx.main.RunOpts{ .out = &stdout_writer.interface, .err = &stderr_writer.interface, .environ_map = init.environ_map }; 213 214 const port = 2222; 215 216 // create priv host key 217 const host_key_file = try std.Io.Dir.cwd().createFile(io, temp_dir_name ++ "/host_key", .{}); 218 defer host_key_file.close(io); 219 try host_key_file.writeStreamingAll(io, 220 \\-----BEGIN OPENSSH PRIVATE KEY----- 221 \\b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNlY2RzYS 222 \\1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQS1ppUfk8n7yvVKEgz3tXjt4q76VGuj 223 \\LcQlRwmogzovV40LLcX0aTObZlQaLWfzJMNpCa/ztMpQlr86nsarE4lEAAAAqLe43zK3uN 224 \\8yAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBLWmlR+TyfvK9UoS 225 \\DPe1eO3irvpUa6MtxCVHCaiDOi9XjQstxfRpM5tmVBotZ/Mkw2kJr/O0ylCWvzqexqsTiU 226 \\QAAAAgQ+LCk30ZNJxb2Da5JL+QOFWCMf7bgXCWcEzhEGGvFWYAAAALcmFkYXJAcm9hcmsB 227 \\AgMEBQ== 228 \\-----END OPENSSH PRIVATE KEY----- 229 \\ 230 ); 231 if (.windows != builtin.os.tag) { 232 try host_key_file.setPermissions(io, @enumFromInt(0o600)); 233 } 234 235 // create priv client key 236 const priv_key_file = try std.Io.Dir.cwd().createFile(io, temp_dir_name ++ "/key", .{}); 237 defer priv_key_file.close(io); 238 try priv_key_file.writeStreamingAll(io, 239 \\-----BEGIN OPENSSH PRIVATE KEY----- 240 \\b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW 241 \\QyNTUxOQAAACCniLPJiaooAWecvOCeAjoJwCSeWxzysvpTNkpYjF22JgAAAJA+7hikPu4Y 242 \\pAAAAAtzc2gtZWQyNTUxOQAAACCniLPJiaooAWecvOCeAjoJwCSeWxzysvpTNkpYjF22Jg 243 \\AAAEDVlopOMnKt/7by/IA8VZvQXUS/O6VLkixOqnnahUdPCKeIs8mJqigBZ5y84J4COgnA 244 \\JJ5bHPKy+lM2SliMXbYmAAAAC3JhZGFyQHJvYXJrAQI= 245 \\-----END OPENSSH PRIVATE KEY----- 246 \\ 247 ); 248 if (.windows != builtin.os.tag) { 249 try priv_key_file.setPermissions(io, @enumFromInt(0o600)); 250 } 251 252 // create pub key 253 const pub_key_file = try std.Io.Dir.cwd().createFile(io, temp_dir_name ++ "/key.pub", .{}); 254 defer pub_key_file.close(io); 255 try pub_key_file.writeStreamingAll(io, 256 \\ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKeIs8mJqigBZ5y84J4COgnAJJ5bHPKy+lM2SliMXbYm radar@roark 257 \\ 258 ); 259 if (.windows != builtin.os.tag) { 260 try pub_key_file.setPermissions(io, @enumFromInt(0o600)); 261 } 262 263 const haxy_bin_path = try std.fs.path.join(allocator, &.{ cwd_path, "zig-out", "bin", "haxy" }); 264 defer allocator.free(haxy_bin_path); 265 266 // dispatch.sh — sshd's forced command. picks between ssh-tui (for 267 // interactive sessions) and ssh-git (for `git clone`/`git push`) by 268 // checking whether sshd populated $SSH_ORIGINAL_COMMAND 269 const dispatch_contents = try std.fmt.allocPrint( 270 allocator, 271 "#!/bin/sh\n" ++ 272 "if [ -z \"$SSH_ORIGINAL_COMMAND\" ]; then\n" ++ 273 " exec {s} ssh-tui --tui-connect 127.0.0.1:8082 --user-key test-user\n" ++ 274 "else\n" ++ 275 " exec {s} ssh-git\n" ++ 276 "fi\n", 277 .{ haxy_bin_path, haxy_bin_path }, 278 ); 279 defer allocator.free(dispatch_contents); 280 281 // scope the file handle so the close happens before sshd ever spawns 282 // a shell that tries to exec it — otherwise Linux raises ETXTBSY. 283 { 284 const dispatch_file = try std.Io.Dir.cwd().createFile(io, temp_dir_name ++ "/dispatch.sh", .{}); 285 defer dispatch_file.close(io); 286 try dispatch_file.writeStreamingAll(io, dispatch_contents); 287 if (.windows != builtin.os.tag) { 288 try dispatch_file.setPermissions(io, @enumFromInt(0o755)); 289 } 290 } 291 292 // create authorized_keys file. forced command runs dispatch.sh so a 293 // single key can drive both the TUI and git operations. 294 const dispatch_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "dispatch.sh" }); 295 defer allocator.free(dispatch_path); 296 297 const auth_keys_contents = try std.fmt.allocPrint( 298 allocator, 299 "command=\"{s}\" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKeIs8mJqigBZ5y84J4COgnAJJ5bHPKy+lM2SliMXbYm radar@roark\n", 300 .{dispatch_path}, 301 ); 302 defer allocator.free(auth_keys_contents); 303 304 const auth_keys_file = try std.Io.Dir.cwd().createFile(io, temp_dir_name ++ "/authorized_keys", .{}); 305 defer auth_keys_file.close(io); 306 try auth_keys_file.writeStreamingAll(io, auth_keys_contents); 307 if (.windows != builtin.os.tag) { 308 try auth_keys_file.setPermissions(io, @enumFromInt(0o600)); 309 } 310 311 // create known_hosts file 312 const known_hosts_file = try std.Io.Dir.cwd().createFile(io, temp_dir_name ++ "/known_hosts", .{}); 313 defer known_hosts_file.close(io); 314 const port_str = std.fmt.comptimePrint("{}", .{port}); 315 try known_hosts_file.writeStreamingAll(io, "[localhost]:" ++ port_str ++ " ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBLWmlR+TyfvK9UoSDPe1eO3irvpUa6MtxCVHCaiDOi9XjQstxfRpM5tmVBotZ/Mkw2kJr/O0ylCWvzqexqsTiUQ="); 316 if (.windows != builtin.os.tag) { 317 try known_hosts_file.setPermissions(io, @enumFromInt(0o600)); 318 } 319 320 // create sshd_config file 321 const sshd_config_str = blk: { 322 // SetEnv PATH=... allows us to propagate the test process's PATH 323 // to the spawned login shell (in sshd). 324 // 325 // without it, shells that don't auto-resource PATH on startup (e.g. nushell 326 // on NixOS) through /etc/set-environment will fail to find 327 // git-upload-pack / git-receive-pack. 328 const base_config = 329 \\AuthenticationMethods publickey 330 \\PubkeyAuthentication yes 331 \\PasswordAuthentication no 332 \\StrictModes no 333 //SetEnv PATH={s} -- if we find $PATH defined. 334 ; 335 var env_map = try std.process.Environ.createMap(init.minimal.environ, allocator); 336 defer env_map.deinit(); 337 338 const config_str = if (env_map.get("PATH")) |path_str| 339 try std.fmt.allocPrint(allocator, "{s}\n" ++ "SetEnv PATH={s}\n", .{ base_config, path_str }) 340 else 341 try std.fmt.allocPrint(allocator, "{s}", .{base_config}); 342 343 break :blk config_str; 344 }; 345 defer allocator.free(sshd_config_str); 346 347 const sshd_config_file = try std.Io.Dir.cwd().createFile(io, temp_dir_name ++ "/sshd_config", .{}); 348 defer sshd_config_file.close(io); 349 try sshd_config_file.writeStreamingAll(io, sshd_config_str); 350 if (.windows != builtin.os.tag) { 351 try sshd_config_file.setPermissions(io, @enumFromInt(0o600)); 352 } 353 354 // create sshd.sh contents 355 const host_key_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "host_key" }); 356 defer allocator.free(host_key_path); 357 const auth_keys_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "authorized_keys" }); 358 defer allocator.free(auth_keys_path); 359 const sshd_contents = try std.fmt.allocPrint( 360 allocator, 361 "#!/bin/sh\nexec $(which sshd) -p {} -f sshd_config -h \"{s}\" -D -e -o AuthorizedKeysFile=\"{s}\" -o PidFile=none", 362 .{ port, host_key_path, auth_keys_path }, 363 ); 364 defer allocator.free(sshd_contents); 365 366 // if path has a space char, it fucks up sshd 367 try std.testing.expect(null == std.mem.indexOfScalar(u8, auth_keys_path, ' ')); 368 369 // create sshd.sh 370 { 371 const sshd_file = try std.Io.Dir.cwd().createFile(io, temp_dir_name ++ "/sshd.sh", .{}); 372 defer sshd_file.close(io); 373 try sshd_file.writeStreamingAll(io, sshd_contents); 374 if (.windows != builtin.os.tag) { 375 try sshd_file.setPermissions(io, .executable_file); 376 } 377 } 378 379 // build a copy-pasteable ssh command line for the user 380 const ssh_key_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "key" }); 381 defer allocator.free(ssh_key_path); 382 const known_hosts_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "known_hosts" }); 383 defer allocator.free(known_hosts_path); 384 385 const ssh_command = try std.fmt.allocPrint( 386 allocator, 387 "ssh -p {} -i {s} -o UserKnownHostsFile={s} -o IdentitiesOnly=yes localhost", 388 .{ port, ssh_key_path, known_hosts_path }, 389 ); 390 defer allocator.free(ssh_command); 391 392 // git uses GIT_SSH_COMMAND to override the ssh invocation it'd 393 // normally do. uses scp-style `host:path` rather than ssh://host/path 394 // so the path stays relative — ssh://host/test would send /test 395 // (absolute), which haxy serve rejects as outside the repo root. 396 const git_command = try std.fmt.allocPrint( 397 allocator, 398 "GIT_SSH_COMMAND='ssh -p {} -i {s} -o UserKnownHostsFile={s} -o IdentitiesOnly=yes' git push localhost:test master", 399 .{ port, ssh_key_path, known_hosts_path }, 400 ); 401 defer allocator.free(git_command); 402 403 const Runnable = struct { 404 io: std.Io, 405 ssh_command: []const u8, 406 git_command: []const u8, 407 408 pub fn run(self: @This()) !void { 409 std.debug.print( 410 \\ 411 \\open the TUI from another terminal with: 412 \\ 413 \\ {s} 414 \\ 415 \\create a git repo and push it over ssh with: 416 \\ 417 \\ mkdir -p temp-try/client/test 418 \\ cd temp-try/client/test 419 \\ git init 420 \\ echo "hello" > hello.txt 421 \\ git add hello.txt 422 \\ git commit -m "let there be light" 423 \\ {s} 424 \\ 425 \\press enter to stop. 426 \\ 427 \\ 428 , .{ self.ssh_command, self.git_command }); 429 430 // launch sshd 431 var process = try std.process.spawn(self.io, .{ 432 .argv = &.{"./sshd.sh"}, 433 .cwd = .{ .path = temp_dir_name }, 434 .stdin = .pipe, 435 .stdout = .inherit, 436 .stderr = .inherit, 437 }); 438 defer process.kill(self.io); 439 440 var buf: [1]u8 = undefined; 441 _ = std.posix.read(std.posix.STDIN_FILENO, &buf) catch {}; 442 } 443 }; 444 445 try srv.run(.xit, .{}, io, allocator, cwd_path, .{ 446 .data_dir = server_path, 447 }, run_opts.err, Runnable{ .io = io, .ssh_command = ssh_command, .git_command = git_command }); 448 } else { 449 var null_writer = std.Io.Writer.Discarding.init(&.{}); 450 const run_opts = hx.main.RunOpts{ .out = &null_writer.writer, .err = &null_writer.writer, .environ_map = init.environ_map }; 451 452 const Runnable = struct { 453 io: std.Io, 454 allocator: std.mem.Allocator, 455 page: *const ui.Page, 456 457 pub fn run(self: @This()) !void { 458 // launch the TUI 459 try hx.ui.run(self.io, self.allocator, self.page); 460 } 461 }; 462 463 try srv.run(.xit, .{}, io, allocator, cwd_path, .{ 464 .data_dir = server_path, 465 }, run_opts.err, Runnable{ .io = io, .allocator = allocator, .page = &page }); 466 } 467 } 468