Skip to content

Commit b89a163

Browse files
authored
Take up openkal 0.11's unit: a handle the caller holds, not a flag (#17)
* 0.6.0 --- one spawn, a working directory, and an honest refusal of the job `CreateProcessW' has taken a current directory all along; what was missing until openkal 0.11 was a caller able to say which. It is now `how->work', obtained through the same `GetFinalPathNameByHandleW' the image path comes from. ⚠️⚠️ KAL_SPAWN_OWN_JOB IS REFUSED, AND NOT BECAUSE THIS SYSTEM CANNOT DO IT --- it is the system the idea comes from. A job object with JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE is exactly the unit the flag describes. What is missing is somewhere to keep the job: `kal_process' is one machine word and it holds the process handle, so `kal_process_terminate' could not reach the job later. The other two implementations need no storage because `getpgid(pid) == pid' recovers the fact from the kernel; this one has no equivalent. ⇒ Claiming it would mean a side table keyed by process handle, and a side table that is wrong under concurrency terminates the WRONG tree. Refused until it can be done without one --- a smaller lie than "kills sometimes". The three unclaimed positions and the calls that refuse them are now written together above kal_process_props, because a word claiming what the next call declines is the disagreement clause 6.2 exists to prevent. * Take up openkal 0.11's unit: a handle the caller holds, not a flag The identity is established at the first start --- a job object created here and its handle reported --- so nothing has to be remembered and no registry appears. `kal_process_terminate' is one program again; the unit has its own operations. * Implement the unit, which the flag form could not be ⭐ THE RESHAPE IS WHAT MADE IT POSSIBLE HERE. Under `KAL_SPAWN_OWN_JOB' this had to recover a job from a process handle and could not; clause 7.1 named that as a defect of the specification rather than of this file. With the unit a handle the caller holds, this environment does it by the route it already offers: a job object is created before it has members, so `kal_process_spawn' assigns the program it starts and `kal_process_job_enter' assigns the caller. ⚠️ JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE IS DELIBERATELY NOT SET, and `SetInformationJobObject' is deliberately not even declared. It would make `kal_process_job_close' end every member --- and closing means only releasing where a unit is a process group, so one operation would mean two things on two systems. Not declaring the call is how that stays true by construction. ⚠️ A program that cannot be put into the unit is ENDED rather than handed back: it would outlive a termination of the unit, which is not the program the caller asked to start. * Answer the stop-request word through the routine this environment already has ⭐ AND THIS IS WHY THE INTERFACE IS A WORD RATHER THAN A HANDLER. The notification here arrives on a context the environment starts for it, which is nothing like a disposition interrupting whatever was running. An interface shaped like the other system's signals would have had to pretend one was the other; a word both can set needs no pretending. The routine stores and wakes and returns false, so the default handling proceeds and a program that never reads the word behaves as it always did. * Export the four Win32 names the new code declares ⚠️ The same guard that caught NtLockFile one release ago: "declared in src/*.h and exported by no .def". Declaring a function this environment provides is half of it; saying which library it comes from is the other half, and omitting it still LINKS through a synthesised thunk --- which is why the check exists rather than being left to the linker.
1 parent abe7c11 commit b89a163

4 files changed

Lines changed: 191 additions & 61 deletions

File tree

mcpp.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[package]
22
namespace = "mcpplibs"
33
name = "openkal-windows"
4-
version = "0.5.0"
4+
version = "0.6.0"
55
description = "An implementation of openkal for Windows, written on the Win32 interfaces and the object manager beneath them, using no C runtime symbol."
66
license = "Apache-2.0"
77

@@ -18,7 +18,7 @@ authors = ["mcpplibs"]
1818
repo = "https://github.com/mcpplibs/openkal-windows"
1919

2020
[dependencies]
21-
openkal = "0.10.0"
21+
openkal = "0.11.0"
2222

2323
# The package contributes definitions and no modules. The interface it
2424
# implements is declared by the specification package, which this package

port/kernel32.def

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ SetHandleInformation
4444
Sleep
4545
SwitchToThread
4646
TerminateProcess
47+
CreateJobObjectW
48+
AssignProcessToJobObject
49+
TerminateJobObject
50+
SetConsoleCtrlHandler
4751
VirtualAlloc
4852
VirtualFree
4953
VirtualProtect

src/process.cpp

Lines changed: 168 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -80,21 +80,87 @@ bool append_quoted(wchar_t* out, okw_uptr cap, okw_uptr& at, const wchar_t* s, o
8080

8181
extern "C" {
8282

83-
int kal_process_spawn(kal_dir base,
83+
// Starting a program. One function since openkal 0.11.
84+
//
85+
// ⚠️ TWO POSITIONS IN `kal_spawn' ARE REFUSED HERE, AND EACH REFUSAL
86+
// IS OLDER THAN 0.11 --- the record moved, the answers did not.
87+
//
88+
// `grants': this environment has no numbering a preopen could arrive under, so
89+
// there is no correspondence to descriptor three. KAL_PROCESS_PROP_GRANT_DIR is
90+
// not claimed. A count of zero asks for a program with no preopens, which is
91+
// what a program here gets anyway, so that request IS answerable and is
92+
// answered.
93+
//
94+
// `KAL_SPAWN_BOUND_LIFETIME': no primitive arms it from inside the started image.
95+
//
96+
// ⭐⭐ AND THE UNIT IS IMPLEMENTED HERE, WHICH AN EARLIER SHAPE OF IT WAS NOT.
97+
//
98+
// 0.11 first spelled this as a flag: make the started program a unit, and let
99+
// `kal_process_terminate' reach the unit afterwards. That shape could not be
100+
// satisfied here. This system can FORM the unit --- a job object is exactly it ---
101+
// but it cannot RECOVER one from a process handle, and `kal_process' is one word
102+
// already holding the process. The other two implementations needed no storage
103+
// because `getpgid(pid) == pid' recovers it from the kernel; this one would have
104+
// needed a registry.
105+
//
106+
// ⚠️ Clause 7.1 states mechanically what needing a registry means: the
107+
// specification "has taken a shape borrowed from one environment, and THE SHAPE
108+
// IS AT FAULT rather than the implementation". handle.h says the same one level
109+
// down --- its array "holds generations and nothing else", and a lookup deciding
110+
// what a word referred to "would be a defect here".
111+
//
112+
// ⇒ So the shape changed rather than this file acquiring a table. The unit is now
113+
// a handle the CALLER holds, whose identity is established at the first start, and
114+
// both kinds of system perform that without remembering anything: here a job
115+
// object is created and its handle reported; where the unit is a process group
116+
// the first member's identifier is reported instead.
117+
//
118+
// ⚠️ JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE IS DELIBERATELY NOT SET. It would make
119+
// `kal_process_job_close' end every member --- and closing means only releasing
120+
// where the unit is a number, so one operation would mean two things. Ending is
121+
// `kal_process_job_terminate' and nothing else is.
122+
int kal_process_spawn(const kal_spawn* how,
84123
const char* path, kal_uintptr path_len,
85124
const char** argv, const kal_uintptr* argv_lens, kal_uintptr argc,
86125
const char** envp, const kal_uintptr* envp_lens, kal_uintptr envc,
87126
const kal_spawn_streams* streams,
88127
kal_process* out) {
89-
void* dir = okw::unpack(base.h);
90-
if (!dir || out == nullptr) return kal_err_invalid;
128+
if (how == nullptr || out == nullptr) return kal_err_invalid;
129+
void* dir = okw::unpack(how->base.h);
130+
void* run = okw::unpack(how->work.h);
131+
if (!dir || !run) return kal_err_invalid;
91132
if (!okw::acceptable(path, path_len)) return kal_err_invalid;
133+
if (how->grant_count > 0) return kal_err_not_supported;
134+
if (how->flags != 0) return kal_err_not_supported;
135+
136+
// The unit, created before its first member and reported to the caller. A
137+
// later member is assigned to the one the caller already holds.
138+
HANDLE unit = nullptr;
139+
bool unit_is_new = false;
140+
if (how->job) {
141+
if (how->job->h != 0) {
142+
unit = okw::unpack(how->job->h);
143+
if (!unit) return kal_err_invalid;
144+
} else {
145+
unit = CreateJobObjectW(nullptr, nullptr);
146+
if (!unit) return okw::translate_win32(GetLastError());
147+
unit_is_new = true;
148+
}
149+
}
150+
struct unit_guard {
151+
HANDLE h; bool own;
152+
~unit_guard() { if (own && h) CloseHandle(h); }
153+
} ug{ unit, unit_is_new };
92154

93155
// The directory's own name, and the program's beneath it.
94156
// Obtained rather than kept in static storage: static storage shared
95157
// between execution contexts would make two concurrent spawns one.
96158
struct scratch {
97159
wchar_t image[okw::kMaxName];
160+
// ⭐ THE DIRECTORY THE PROGRAM RUNS IN, WHICH IS NOT THE ONE IT IS NAMED
161+
// FROM. `CreateProcessW' has taken a current directory all along; what
162+
// was missing until 0.11 was a caller able to say which.
163+
wchar_t cwd[okw::kMaxName];
98164
wchar_t line[kCommandLine];
99165
wchar_t block[kCommandLine];
100166
};
@@ -118,6 +184,13 @@ int kal_process_spawn(kal_dir base,
118184
}
119185
image[at] = 0;
120186

187+
// The same enquiry the image path comes from, upon the other directory.
188+
wchar_t* cwd = work->cwd;
189+
const DWORD cn = GetFinalPathNameByHandleW(run, cwd, okw::kMaxName - 1,
190+
FILE_NAME_NORMALIZED | VOLUME_NAME_DOS);
191+
if (cn == 0 || cn >= okw::kMaxName - 1) return okw::translate_win32(GetLastError());
192+
cwd[cn] = 0;
193+
121194
// The vector, unaltered, including its first element.
122195
wchar_t* line = work->line;
123196
okw_uptr used = 0;
@@ -170,13 +243,96 @@ int kal_process_spawn(kal_dir base,
170243
const BOOL started = CreateProcessW(image, argc ? line : nullptr, nullptr, nullptr,
171244
inherit ? TRUE : FALSE,
172245
CREATE_UNICODE_ENVIRONMENT,
173-
envc ? block : nullptr, nullptr, &startup, &info);
246+
envc ? block : nullptr, cwd, &startup, &info);
174247
if (!started) return okw::translate_win32(GetLastError());
175248
CloseHandle(info.hThread);
249+
250+
// ⚠️ ASSIGNED BEFORE THE CALLER IS TOLD ANYTHING. A program that could not be
251+
// put into the unit is not the program that was asked for --- it would outlive
252+
// a termination of the unit --- so it is ended rather than handed back.
253+
if (unit && !AssignProcessToJobObject(unit, info.hProcess)) {
254+
const DWORD why = GetLastError();
255+
TerminateProcess(info.hProcess, 127);
256+
CloseHandle(info.hProcess);
257+
return okw::translate_win32(why);
258+
}
259+
260+
// The caller's word, written only now, and only for a unit this start made.
261+
if (unit_is_new) { how->job->h = okw::pack(unit); ug.own = false; }
262+
176263
*out = kal_process{ okw::pack(info.hProcess) };
177264
return kal_ok;
178265
}
179266

267+
// ⭐⭐ A WORD THIS ENVIRONMENT SETS WHEN SOMEBODY HAS ASKED THIS PROGRAM TO END.
268+
//
269+
// ⚠️ AND THIS IS WHY THE INTERFACE IS A WORD RATHER THAN A HANDLER. The
270+
// notification here arrives ON A CONTEXT OF ITS OWN --- the environment starts one
271+
// to run the routine --- which is nothing like a disposition interrupting whatever
272+
// was running. An interface shaped like the other system's signals would have
273+
// had to pretend one was the other; a word both can set needs no pretending.
274+
//
275+
// The routine stores and wakes, which is all `kal_task_wait' needs on the other
276+
// side. Returning false lets the default handling proceed, so a program that
277+
// never reads the word behaves as it always did.
278+
namespace {
279+
kal_u32 g_stop_word = 0;
280+
int g_stop_armed = 0;
281+
282+
BOOL OKW_API stop_routine(DWORD) {
283+
g_stop_word = 1;
284+
WakeByAddressAll(&g_stop_word);
285+
return FALSE;
286+
}
287+
} // namespace
288+
289+
// ⚠️ Armed on the first enquiry, so that adding this operation changes nothing
290+
// for a program that does not use it.
291+
const kal_u32* kal_process_stop_requested(void) {
292+
if (!g_stop_armed) { g_stop_armed = 1; SetConsoleCtrlHandler(stop_routine, TRUE); }
293+
return &g_stop_word;
294+
}
295+
296+
// This program itself joins or forms a unit. ⭐ NATURAL HERE TOO, and by the
297+
// route this environment already offers: a job object is created before it has
298+
// members, so the caller simply becomes its first one.
299+
int kal_process_job_enter(kal_job* j) {
300+
if (j == nullptr) return kal_err_invalid;
301+
HANDLE unit = nullptr;
302+
bool made = false;
303+
if (j->h != 0) {
304+
unit = okw::unpack(j->h);
305+
if (!unit) return kal_err_invalid;
306+
} else {
307+
unit = CreateJobObjectW(nullptr, nullptr);
308+
if (!unit) return okw::translate_win32(GetLastError());
309+
made = true;
310+
}
311+
if (!AssignProcessToJobObject(unit, GetCurrentProcess())) {
312+
const DWORD why = GetLastError();
313+
if (made) CloseHandle(unit);
314+
return okw::translate_win32(why);
315+
}
316+
if (made) j->h = okw::pack(unit);
317+
return kal_ok;
318+
}
319+
320+
// Every program in the unit. A job ends its members as one, which is the whole
321+
// reason this environment's job object is the right thing to build a unit from.
322+
int kal_process_job_terminate(kal_job j) {
323+
HANDLE h = okw::unpack(j.h);
324+
if (!h) return kal_err_invalid;
325+
if (!TerminateJobObject(h, 15)) return okw::translate_win32(GetLastError());
326+
return kal_ok;
327+
}
328+
329+
// ⚠️ RELEASES AND DOES NOT END. The limit that would have ended the members on
330+
// the last close is deliberately not set --- see the note above kal_process_spawn.
331+
void kal_process_job_close(kal_job j) {
332+
HANDLE h = okw::unpack(j.h);
333+
if (h) { okw::retire(j.h); CloseHandle(h); }
334+
}
335+
180336
// A channel: a pair of streams of which one end is meant to cross a spawn.
181337
//
182338
// THIS ENVIRONMENT DECIDES INHERITANCE PER HANDLE AND NOT PER EXEC, which is the
@@ -221,36 +377,6 @@ void kal_process_channel_close(kal_stream s) {
221377
CloseHandle(h);
222378
}
223379

224-
// Starting a program that receives exactly the directories named.
225-
//
226-
// ⚠️ NOT PROVIDED, AND THE REFUSAL IS THE HONEST ANSWER RATHER THAN A GAP. A
227-
// preopened directory is a handle a started program reads back through
228-
// kal_fs_preopen by NUMBER, and this environment has no numbering: a handle
229-
// crosses a spawn by being inheritable, and the started program learns of it
230-
// through a mechanism the parent has to arrange itself. There is no
231-
// correspondence here to descriptor three.
232-
//
233-
// Clause 6.2 is what makes the refusal conforming rather than a deviation: the
234-
// operation exists, reports kal_err_not_supported, and the property word does
235-
// not claim KAL_PROCESS_PROP_GRANT_DIR. A caller therefore learns from the word
236-
// what it would otherwise learn from a failed call.
237-
int kal_process_spawn_with(kal_dir base,
238-
const char* path, kal_uintptr path_len,
239-
const char** argv, const kal_uintptr* argv_lens, kal_uintptr argc,
240-
const char** envp, const kal_uintptr* envp_lens, kal_uintptr envc,
241-
const kal_spawn_streams* streams,
242-
const kal_preopen* grants, kal_uintptr grant_count,
243-
kal_process* out) {
244-
// A count of zero asks for a program with no preopens, which this
245-
// environment gives a started program anyway --- it has none to pass. That
246-
// request is therefore answerable, and is answered by the ordinary spawn.
247-
if (grant_count == 0)
248-
return kal_process_spawn(base, path, path_len,
249-
argv, argv_lens, argc,
250-
envp, envp_lens, envc, streams, out);
251-
(void)grants;
252-
return kal_err_not_supported;
253-
}
254380

255381
int kal_process_wait(kal_process p, int* status, int* terminated) {
256382
void* h = okw::unpack(p.h);
@@ -287,34 +413,17 @@ void kal_process_close(kal_process p) {
287413
if (h) { okw::retire(p.h); CloseHandle(h); }
288414
}
289415

290-
// KAL_PROCESS_PROP_GRANT_DIR is deliberately absent: kal_process_spawn_with
291-
// refuses a non-empty set of grants here, and a word claiming a facility the
292-
// next call refuses is the disagreement clause 6.2 exists to prevent.
293-
// Starting a program whose lifetime is bound to this one's. Version 0.10.
294-
//
295-
// ⚠️⚠️ REFUSED HERE, AND NOT BECAUSE THIS SYSTEM CANNOT --- IT CAN. A job object
296-
// with `JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE' ends every program in the job when
297-
// the last handle to it closes, which this system does when a process dies
298-
// however it dies. That is exactly the binding openkal describes.
299-
//
300-
// ⚠️ IT IS NOT CLAIMED IN THIS RELEASE BECAUSE IT HAS NOT BEEN MEASURED HERE.
301-
// The one consumer that needs it composes `execve', and this system already
302-
// declines `openkal.space' --- so nothing on this system reaches the operation
303-
// today, and claiming a binding that has never been exercised is the shape of
304-
// answer openkal exists to refuse. It is the next thing this implementation
305-
// should do, and it is recorded as that rather than as an absence.
416+
// ⚠️ THREE POSITIONS ARE DELIBERATELY ABSENT, AND EACH IS ABSENT BECAUSE THE
417+
// NEXT CALL REFUSES IT. A word claiming a facility the operation then declines is
418+
// the disagreement clause 6.2 exists to prevent, so the two are written together
419+
// and read together:
306420
//
307-
// A caller that asks `kal_process_props' first is told before it depends on it.
308-
int kal_process_spawn_bound(kal_dir, const char*, kal_uintptr,
309-
const char**, const kal_uintptr*, kal_uintptr,
310-
const char**, const kal_uintptr*, kal_uintptr,
311-
const kal_spawn_streams*, kal_process*) {
312-
return kal_err_not_supported;
313-
}
314-
421+
// GRANT_DIR kal_process_spawn refuses a non-empty `grants'
422+
// BOUND_LIFETIME no primitive arms it from inside the started image
315423
kal_uintptr kal_process_props(void) { return
316424
KAL_PROCESS_PROP_TERMINATE | KAL_PROCESS_PROP_STREAM_PASSING
317425
| KAL_PROCESS_PROP_EXIT_STATUS
318-
| KAL_PROCESS_PROP_CHANNEL; }
426+
| KAL_PROCESS_PROP_CHANNEL
427+
| KAL_PROCESS_PROP_JOB; }
319428

320429
}

src/win32.h

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,23 @@ OKW_IMPORT BOOL OKW_API CreateProcessW(LPCWSTR, LPWSTR, SECURITY_ATTRIBUTES*,
304304
LPCWSTR, STARTUPINFOW*, PROCESS_INFORMATION*);
305305
OKW_IMPORT BOOL OKW_API GetExitCodeProcess(HANDLE, DWORD*);
306306
OKW_IMPORT BOOL OKW_API TerminateProcess(HANDLE, UINT);
307+
308+
// openkal 0.11: the unit a set of started programs forms. A job object ends its
309+
// members as one, which is what `kal_process_job_terminate' is.
310+
//
311+
// ⚠️ NO `SetInformationJobObject' HERE, AND ITS ABSENCE IS THE DESIGN. The limit
312+
// that ends members when the last handle closes --- JOB_OBJECT_LIMIT_KILL_ON_JOB_
313+
// CLOSE --- is exactly what must NOT be set: `kal_process_job_close' releases and
314+
// does not end, because where a unit is a process group closing is releasing a
315+
// number. Not declaring the call is how that stays true by construction.
316+
OKW_IMPORT HANDLE OKW_API CreateJobObjectW(SECURITY_ATTRIBUTES*, LPCWSTR);
317+
OKW_IMPORT BOOL OKW_API AssignProcessToJobObject(HANDLE, HANDLE);
318+
OKW_IMPORT BOOL OKW_API TerminateJobObject(HANDLE, UINT);
319+
320+
// openkal 0.11: the word set when this program is asked to end. The routine runs
321+
// on a context this environment starts, which is why the interface is a word and
322+
// not a disposition --- see kal_process_stop_requested.
323+
OKW_IMPORT BOOL OKW_API SetConsoleCtrlHandler(BOOL (OKW_API*)(DWORD), BOOL);
307324
OKW_IMPORT HANDLE OKW_API GetCurrentProcess(void);
308325
OKW_IMPORT DWORD OKW_API WaitForSingleObject(HANDLE, DWORD);
309326

0 commit comments

Comments
 (0)