summaryrefslogtreecommitdiff
path: root/src/processrunner.h
blob: c5b6bbe96c0e814d1ab42f38be4f4002c42c7c46 (plain)
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
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
#ifndef PROCESSRUNNER_H
#define PROCESSRUNNER_H

#include "envmodule.h"
#include "spawn.h"
#include "uilocker.h"
#include <executableinfo.h>

class OrganizerCore;
class IUserInterface;
class Executable;
class MOShortcut;

// handles spawning a process and waiting for it, including setting up the lock
// widget if required
//
class ProcessRunner
{
public:
  enum Results
  {
    // the process is still running
    Running = 1,

    // the process has run to completion
    Completed,

    // the process couldn't be started or waited for
    Error,

    // the user has clicked the cancel button in the lock widget
    Cancelled,

    // the user has clicked the unlock button in the lock widget
    ForceUnlocked
  };

  enum WaitFlag
  {
    NoFlags = 0x00,

    // the directory structure will be refreshed once the process has completed
    TriggerRefresh = 0x01,

    // the process will be waited for even if locking is disabled or the
    // process is not hooked
    ForceWait = 0x02,

    // only valid with TriggerRefresh; run() will block until the refresh has
    // completed
    WaitForRefresh = 0x04,

    // combination of flags used to run programs from the command line
    //
    //  1) TriggerRefresh: MO must refresh after running the program because
    //     programs can modify files behind its back; for example, external
    //     LOOT will modify loadorder.txt, so MO must read it back or it will
    //     write back the old order when exiting
    //
    //  2) WaitForRefresh: refreshing is asynchronous, so the refresh must
    //     complete before MO exits or stale data might be written to disk
    //
    //  3) ForceWait: MO must wait for the program to finish even if locking the
    //     ui is disabled
    ForCommandLine = TriggerRefresh | WaitForRefresh | ForceWait
  };

  using WaitFlags       = QFlags<WaitFlag>;
  using ForcedLibraries = QList<MOBase::ExecutableForcedLoadSetting>;

  ProcessRunner(OrganizerCore& core, IUserInterface* ui);

  // move only
  ProcessRunner(ProcessRunner&&)                 = default;
  ProcessRunner& operator=(const ProcessRunner&) = delete;
  ProcessRunner(const ProcessRunner&)            = delete;
  ProcessRunner& operator=(ProcessRunner&&)      = delete;

  ProcessRunner& setBinary(const QFileInfo& binary);
  ProcessRunner& setArguments(const QString& arguments);
  ProcessRunner& setCurrentDirectory(const QDir& directory);
  ProcessRunner& setSteamID(const QString& steamID);
  ProcessRunner& setCustomOverwrite(const QString& customOverwrite);
  ProcessRunner& setForcedLibraries(const ForcedLibraries& forcedLibraries);
  ProcessRunner& setProfileName(const QString& profileName);
  ProcessRunner& setWaitForCompletion(WaitFlags flags          = NoFlags,
                                      UILocker::Reasons reason = UILocker::LockUI);
  ProcessRunner& setHooked(bool b);

  // - if the target is an executable file, runs it hooked
  // - if the target is a file:
  //     - if forceHook is false, calls ShellExecute() on it
  //     - if forceHook is true, gets the executable associated with the file
  //       and runs that hooked by passing the file as an argument
  //
  ProcessRunner& setFromFile(QWidget* parent, const QFileInfo& targetInfo);

  ProcessRunner& setFromExecutable(const Executable& exe);
  ProcessRunner& setFromShortcut(const MOShortcut& shortcut);

  // this is a messy one that's used for running an arbitrary file from the
  // command line, or by plugins (see OrganizerProxy::startApplication())
  //
  // 1) if `executable` contains a path separator, it's treated as a binary on
  //    disk and will be launched with given settings; it's also looked up in
  //    the list of configured executables, which sets the steam ID and forced
  //    libraries
  //
  // 2) if `executable` has no path separators, it's treated purely as an
  //    executable, but its arguments, current directory and custom overwrite
  //    can also be overridden
  //
  //    if the executable is not found in the list, the binary is run solely
  //    based on the parameters given
  //
  ProcessRunner& setFromFileOrExecutable(const QString& executable,
                                         const QStringList& args,
                                         const QString& cwd                   = {},
                                         const QString& profile               = {},
                                         const QString& forcedCustomOverwrite = {},
                                         bool ignoreCustomOverwrite           = false);

  // spawns the process and waits for it if required
  //
  Results run();

  // takes ownership of the given handle and waits for it if required
  //
  Results attachToProcess(HANDLE h);

  // exit code of the process, will return -1 if the process wasn't waited for
  //
  DWORD exitCode() const;

  // this may be INVALID_HANDLE_VALUE if:
  //
  //  1) no process was started, or
  //  2) the process was started successfully, but the system didn't return a
  //     handle for it; this can happen for inproc handlers, for example, such
  //     the photo viewer
  //
  // note that the handle is still owned by this ProcessRunner and will be
  // closed when destroyed; see stealProcessHandle()
  //
  HANDLE getProcessHandle() const;

  // releases ownership of the process handle; if this is called after the
  // process is completed, exitCode() will still return the correct value
  //
  env::HandlePtr stealProcessHandle();

  // waits for all usvfs processes spawned by this instance of MO; returns
  // immediately with ForceUnlocked if locking is disabled
  //
  // strictly speaking, this shouldn't be here, as it has nothing to do with
  // running a process, but it uses the same internal stuff as when running a
  // process
  //
  Results waitForAllUSVFSProcessesWithLock(UILocker::Reasons reason);

private:
  OrganizerCore& m_core;
  IUserInterface* m_ui;
  spawn::SpawnParameters m_sp;
  QString m_customOverwrite;
  ForcedLibraries m_forcedLibraries;
  QString m_profileName;
  UILocker::Reasons m_lockReason;
  WaitFlags m_waitFlags;
  QFileInfo m_shellOpen;
  env::HandlePtr m_handle;
  DWORD m_exitCode;

  bool shouldRunShell() const;
  bool shouldRefresh(Results r) const;

  // runs the command in m_shellOpen; returns empty if it can be waited for
  //
  std::optional<Results> runShell();

  // runs the binary; returns empty if it can be waited for
  //
  std::optional<Results> runBinary();

  // waits for process completion if required
  //
  Results postRun();

  // creates the lock widget and calls f()
  //
  void withLock(std::function<void(UILocker::Session&)> f);
};

Q_DECLARE_OPERATORS_FOR_FLAGS(ProcessRunner::WaitFlags);

#endif  // PROCESSRUNNER_H