openMSX
DiskChanger.cc
Go to the documentation of this file.
1 #include "DiskChanger.hh"
2 #include "DiskFactory.hh"
3 #include "DummyDisk.hh"
4 #include "RamDSKDiskImage.hh"
5 #include "DirAsDSK.hh"
6 #include "CommandController.hh"
8 #include "Scheduler.hh"
9 #include "FilePool.hh"
10 #include "File.hh"
11 #include "MSXMotherBoard.hh"
12 #include "Reactor.hh"
13 #include "DiskManipulator.hh"
14 #include "FileContext.hh"
15 #include "FileOperations.hh"
16 #include "FileException.hh"
17 #include "CommandException.hh"
18 #include "CliComm.hh"
19 #include "TclObject.hh"
20 #include "EmuTime.hh"
21 #include "serialize.hh"
22 #include "serialize_stl.hh"
23 #include "serialize_constr.hh"
24 #include "strCat.hh"
25 #include "view.hh"
26 #include <functional>
27 #include <memory>
28 #include <utility>
29 
30 using std::string;
31 
32 namespace openmsx {
33 
35  string driveName_,
36  bool createCmd,
37  bool doubleSidedDrive_,
38  std::function<void()> preChangeCallback_)
39  : reactor(board.getReactor())
40  , controller(board.getCommandController())
41  , stateChangeDistributor(&board.getStateChangeDistributor())
42  , scheduler(&board.getScheduler())
43  , preChangeCallback(std::move(preChangeCallback_))
44  , driveName(std::move(driveName_))
45  , doubleSidedDrive(doubleSidedDrive_)
46 {
47  init(tmpStrCat(board.getMachineID(), "::"), createCmd);
48 }
49 
50 DiskChanger::DiskChanger(Reactor& reactor_, string driveName_)
51  : reactor(reactor_)
52  , controller(reactor.getCommandController())
53  , stateChangeDistributor(nullptr)
54  , scheduler(nullptr)
55  , driveName(std::move(driveName_))
56  , doubleSidedDrive(true) // irrelevant, but needs a value
57 {
58  init({}, true);
59 }
60 
61 void DiskChanger::init(std::string_view prefix, bool createCmd)
62 {
63  if (createCmd) createCommand();
64  ejectDisk();
65  auto& manipulator = reactor.getDiskManipulator();
66  manipulator.registerDrive(*this, prefix);
67  if (stateChangeDistributor) {
68  stateChangeDistributor->registerListener(*this);
69  }
70 }
71 
73 {
74  if (diskCommand) return;
75  diskCommand.emplace(controller, *this);
76 }
77 
79 {
80  if (stateChangeDistributor) {
81  stateChangeDistributor->unregisterListener(*this);
82  }
83  auto& manipulator = reactor.getDiskManipulator();
84  manipulator.unregisterDrive(*this);
85 }
86 
88 {
89  return disk->getName();
90 }
91 
93 {
94  bool ret = diskChangedFlag;
95  diskChangedFlag = false;
96  return ret;
97 }
98 
100 {
101  if (dynamic_cast<DummyDisk*>(disk.get())) {
102  return nullptr;
103  }
104  return dynamic_cast<SectorAccessibleDisk*>(disk.get());
105 }
106 
107 std::string_view DiskChanger::getContainerName() const
108 {
109  return getDriveName();
110 }
111 
112 void DiskChanger::sendChangeDiskEvent(span<const TclObject> args)
113 {
114  // note: might throw MSXException
115  if (stateChangeDistributor) {
116  stateChangeDistributor->distributeNew<MSXCommandEvent>(
117  scheduler->getCurrentTime(), args);
118  } else {
119  execute(args);
120  }
121 }
122 
123 void DiskChanger::signalStateChange(const StateChange& event)
124 {
125  const auto* commandEvent = dynamic_cast<const MSXCommandEvent*>(&event);
126  if (!commandEvent) return;
127 
128  execute(commandEvent->getTokens());
129 }
130 
131 void DiskChanger::execute(span<const TclObject> tokens)
132 {
133  if (tokens[0] == getDriveName()) {
134  if (tokens[1] == "eject") {
135  ejectDisk();
136  } else {
137  insertDisk(tokens); // might throw
138  }
139  }
140 }
141 
142 void DiskChanger::stopReplay(EmuTime::param /*time*/) noexcept
143 {
144  // nothing
145 }
146 
147 int DiskChanger::insertDisk(const std::string& filename)
148 {
149  TclObject args[] = { TclObject("dummy"), TclObject(filename) };
150  try {
151  insertDisk(args);
152  return 0;
153  } catch (MSXException&) {
154  return -1;
155  }
156 }
157 
159 {
160  string diskImage = FileOperations::getConventionalPath(string(args[1].getString()));
161  auto& diskFactory = reactor.getDiskFactory();
162  std::unique_ptr<Disk> newDisk(diskFactory.createDisk(diskImage, *this));
163  for (const auto& arg : view::drop(args, 2)) {
164  newDisk->applyPatch(Filename(
165  arg.getString(), userFileContext()));
166  }
167 
168  // no errors, only now replace original disk
169  changeDisk(std::move(newDisk));
170 }
171 
172 void DiskChanger::ejectDisk()
173 {
174  changeDisk(std::make_unique<DummyDisk>());
175 }
176 
177 void DiskChanger::changeDisk(std::unique_ptr<Disk> newDisk)
178 {
179  if (preChangeCallback) preChangeCallback();
180  disk = std::move(newDisk);
181  diskChangedFlag = true;
183  getDiskName().getResolved());
184 }
185 
186 
187 // class DiskCommand
188 
190  DiskChanger& diskChanger_)
191  : Command(commandController_, diskChanger_.driveName)
192  , diskChanger(diskChanger_)
193 {
194 }
195 
197 {
198  if (tokens.size() == 1) {
199  result.addListElement(tmpStrCat(diskChanger.getDriveName(), ':'),
200  diskChanger.getDiskName().getResolved());
201 
202  TclObject options;
203  if (dynamic_cast<DummyDisk*>(diskChanger.disk.get())) {
204  options.addListElement("empty");
205  } else if (dynamic_cast<DirAsDSK*>(diskChanger.disk.get())) {
206  options.addListElement("dirasdisk");
207  } else if (dynamic_cast<RamDSKDiskImage*>(diskChanger.disk.get())) {
208  options.addListElement("ramdsk");
209  }
210  if (diskChanger.disk->isWriteProtected()) {
211  options.addListElement("readonly");
212  }
213  if (options.getListLength(getInterpreter()) != 0) {
214  result.addListElement(options);
215  }
216 
217  } else if (tokens[1] == "ramdsk") {
218  TclObject args[] = {
219  TclObject(diskChanger.getDriveName()), tokens[1]
220  };
221  diskChanger.sendChangeDiskEvent(args);
222  } else if (tokens[1] == "-ramdsk") {
223  TclObject args[] = {TclObject(diskChanger.getDriveName()), TclObject("ramdsk")};
224  diskChanger.sendChangeDiskEvent(args);
225  result = "Warning: use of '-ramdsk' is deprecated, instead use the 'ramdsk' subcommand";
226  } else if (tokens[1] == "-eject") {
227  TclObject args[] = {TclObject(diskChanger.getDriveName()), TclObject("eject")};
228  diskChanger.sendChangeDiskEvent(args);
229  result = "Warning: use of '-eject' is deprecated, instead use the 'eject' subcommand";
230  } else if (tokens[1] == "eject") {
231  TclObject args[] = {TclObject(diskChanger.getDriveName()), TclObject("eject")};
232  diskChanger.sendChangeDiskEvent(args);
233  } else {
234  int firstFileToken = 1;
235  if (tokens[1] == "insert") {
236  if (tokens.size() > 2) {
237  firstFileToken = 2; // skip this subcommand as filearg
238  } else {
239  throw CommandException("Missing argument to insert subcommand");
240  }
241  }
242  try {
243  std::vector<TclObject> args = { TclObject(diskChanger.getDriveName()) };
244  for (size_t i = firstFileToken; i < tokens.size(); ++i) { // 'i' changes in loop
245  std::string_view option = tokens[i].getString();
246  if (option == "-ips") {
247  if (++i == tokens.size()) {
248  throw MSXException(
249  "Missing argument for option \"", option, '\"');
250  }
251  args.emplace_back(tokens[i]);
252  } else {
253  // backwards compatibility
254  args.emplace_back(option);
255  }
256  }
257  diskChanger.sendChangeDiskEvent(args);
258  } catch (FileException& e) {
259  throw CommandException(std::move(e).getMessage());
260  }
261  }
262 }
263 
264 string DiskCommand::help(span<const TclObject> /*tokens*/) const
265 {
266  const string& driveName = diskChanger.getDriveName();
267  return strCat(
268  driveName, " eject : remove disk from virtual drive\n",
269  driveName, " ramdsk : create a virtual disk in RAM\n",
270  driveName, " insert <filename> : change the disk file\n",
271  driveName, " <filename> : change the disk file\n",
272  driveName, " : show which disk image is in drive\n"
273  "The following options are supported when inserting a disk image:\n"
274  "-ips <filename> : apply the given IPS patch to the disk image");
275 }
276 
277 void DiskCommand::tabCompletion(std::vector<string>& tokens) const
278 {
279  if (tokens.size() >= 2) {
280  using namespace std::literals;
281  static constexpr std::array extra = {
282  "eject"sv, "ramdsk"sv, "insert"sv,
283  };
284  completeFileName(tokens, userFileContext(), extra);
285  }
286 }
287 
289 {
290  return tokens.size() > 1;
291 }
292 
293 static string calcSha1(SectorAccessibleDisk* disk, FilePool& filePool)
294 {
295  return disk ? disk->getSha1Sum(filePool).toString() : string{};
296 }
297 
298 // version 1: initial version
299 // version 2: replaced Filename with DiskName
300 template<typename Archive>
301 void DiskChanger::serialize(Archive& ar, unsigned version)
302 {
303  DiskName diskname = disk->getName();
304  if (ar.versionBelow(version, 2)) {
305  // there was no DiskName yet, just a plain Filename
307  ar.serialize("disk", filename);
308  if (filename.getOriginal() == "ramdisk") {
309  diskname = DiskName(Filename(), "ramdisk");
310  } else {
311  diskname = DiskName(filename, {});
312  }
313  } else {
314  ar.serialize("disk", diskname);
315  }
316 
317  std::vector<Filename> patches;
318  if constexpr (!Archive::IS_LOADER) {
319  patches = disk->getPatches();
320  }
321  ar.serialize("patches", patches);
322 
323  auto& filePool = reactor.getFilePool();
324  string oldChecksum;
325  if constexpr (!Archive::IS_LOADER) {
326  oldChecksum = calcSha1(getSectorAccessibleDisk(), filePool);
327  }
328  ar.serialize("checksum", oldChecksum);
329 
330  if constexpr (Archive::IS_LOADER) {
331  diskname.updateAfterLoadState();
332  string name = diskname.getResolved(); // TODO use Filename
333  if (!name.empty()) {
334  // Only when the original file doesn't exist on this
335  // system, try to search by sha1sum. This means we
336  // prefer the original file over a file with a matching
337  // sha1sum (the original file may have changed). An
338  // alternative is to prefer the exact sha1sum match.
339  // I'm not sure which alternative is better.
340  if (!FileOperations::exists(name)) {
341  assert(!oldChecksum.empty());
342  auto file = filePool.getFile(
343  FileType::DISK, Sha1Sum(oldChecksum));
344  if (file.is_open()) {
345  name = file.getURL();
346  }
347  }
348  std::vector<TclObject> args =
349  { TclObject("dummy"), TclObject(name) };
350  for (auto& p : patches) {
351  p.updateAfterLoadState();
352  args.emplace_back(p.getResolved()); // TODO
353  }
354 
355  try {
356  insertDisk(args);
357  } catch (MSXException& e) {
358  throw MSXException(
359  "Couldn't reinsert disk in drive ",
360  getDriveName(), ": ", e.getMessage());
361  // Alternative: Print warning and continue
362  // without diskimage. Is this better?
363  }
364  }
365 
366  string newChecksum = calcSha1(getSectorAccessibleDisk(), filePool);
367  if (oldChecksum != newChecksum) {
368  controller.getCliComm().printWarning(
369  "The content of the diskimage ",
370  diskname.getResolved(),
371  " has changed since the time this savestate was "
372  "created. This might result in emulation problems "
373  "or even diskcorruption. To prevent the latter, "
374  "the disk is now write-protected (eject and "
375  "reinsert the disk if you want to override this).");
376  disk->forceWriteProtect();
377  }
378  }
379 
380  // This should only be restored after disk is inserted
381  ar.serialize("diskChanged", diskChangedFlag);
382 }
383 
384 // extra (local) constructor arguments for polymorphic de-serialization
386 {
387  using type = std::tuple<std::string>;
388 
389  template<typename Archive>
390  void save(Archive& ar, const DiskChanger& changer)
391  {
392  ar.serialize("driveName", changer.getDriveName());
393  }
394 
395  template<typename Archive> type load(Archive& ar, unsigned /*version*/)
396  {
397  string driveName;
398  ar.serialize("driveName", driveName);
399  return std::tuple(driveName);
400  }
401 };
402 
405  std::reference_wrapper<MSXMotherBoard>);
406 
407 } // namespace openmsx
virtual void update(UpdateType type, std::string_view name, std::string_view value)=0
Interpreter & getInterpreter() const final
Definition: Command.cc:38
virtual CliComm & getCliComm()=0
static void completeFileName(std::vector< std::string > &tokens, const FileContext &context, const RANGE &extra)
Definition: Completer.hh:147
const DiskName & getDiskName() const
Definition: DiskChanger.cc:87
~DiskChanger() override
Definition: DiskChanger.cc:78
int insertDisk(const std::string &filename) override
Definition: DiskChanger.cc:147
const std::string & getDriveName() const
Definition: DiskChanger.hh:54
std::string_view getContainerName() const override
Definition: DiskChanger.cc:107
void serialize(Archive &ar, unsigned version)
Definition: DiskChanger.cc:301
DiskChanger(MSXMotherBoard &board, std::string driveName, bool createCmd=true, bool doubleSidedDrive=true, std::function< void()> preChangeCallback={})
Definition: DiskChanger.cc:34
void changeDisk(std::unique_ptr< Disk > newDisk)
Definition: DiskChanger.cc:177
SectorAccessibleDisk * getSectorAccessibleDisk() override
Definition: DiskChanger.cc:99
bool diskChanged() override
Definition: DiskChanger.cc:92
bool needRecord(span< const TclObject > tokens) const
Definition: DiskChanger.cc:288
void execute(span< const TclObject > tokens, TclObject &result) override
Execute this command.
Definition: DiskChanger.cc:196
std::string help(span< const TclObject > tokens) const override
Print help for this command.
Definition: DiskChanger.cc:264
void tabCompletion(std::vector< std::string > &tokens) const override
Attempt tab completion for this command.
Definition: DiskChanger.cc:277
DiskCommand(CommandController &commandController, DiskChanger &diskChanger)
Definition: DiskChanger.cc:189
void unregisterDrive(DiskContainer &drive)
void registerDrive(DiskContainer &drive, std::string_view prefix)
std::string getResolved() const
Definition: DiskName.cc:22
void updateAfterLoadState()
Definition: DiskName.cc:27
File getFile(FileType fileType, const Sha1Sum &sha1sum)
Search file with the given sha1sum.
Definition: FilePool.cc:53
const std::string & getURL() const
Returns the URL of this file object.
Definition: File.cc:136
This class represents a filename.
Definition: Filename.hh:18
This class is used to for Tcl commands that directly influence the MSX state (e.g.
const std::string & getMessage() const &
Definition: MSXException.hh:23
std::string_view getMachineID() const
Contains the main loop of openMSX.
Definition: Reactor.hh:68
DiskFactory & getDiskFactory()
Definition: Reactor.hh:88
DiskManipulator & getDiskManipulator()
Definition: Reactor.hh:89
EmuTime::param getCurrentTime() const
Get the current scheduler time.
Definition: Scheduler.cc:84
Sha1Sum getSha1Sum(FilePool &filepool)
Calculate SHA1 of the content of this disk.
std::vector< Filename > getPatches() const
This class represents the result of a sha1 calculation (a 160-bit value).
Definition: sha1.hh:22
std::string toString() const
Definition: utils/sha1.cc:230
void registerListener(StateChangeListener &listener)
(Un)registers the given object to receive state change events.
void distributeNew(EmuTime::param time, Args &&...args)
Deliver the event to all registered listeners MSX input devices should call the distributeNew() versi...
void unregisterListener(StateChangeListener &listener)
unsigned getListLength(Interpreter &interp) const
Definition: TclObject.cc:125
void addListElement(const T &t)
Definition: TclObject.hh:129
Definition: span.hh:126
constexpr index_type size() const noexcept
Definition: span.hh:296
bool exists(zstring_view filename)
Does this file (directory) exists?
const std::string & getConventionalPath(const std::string &path)
Returns the path in conventional path-delimiter.
This file implemented 3 utility functions:
Definition: Autofire.cc:9
constexpr const char *const filename
REGISTER_POLYMORPHIC_CLASS_1(DiskContainer, DiskChanger, "DiskChanger", std::reference_wrapper< MSXMotherBoard >)
FileContext userFileContext(string_view savePath)
Definition: FileContext.cc:173
constexpr auto drop(Range &&range, size_t n)
Definition: view.hh:373
#define INSTANTIATE_SERIALIZE_METHODS(CLASS)
Definition: serialize.hh:998
TemporaryString tmpStrCat(Ts &&... ts)
Definition: strCat.hh:659
std::string strCat(Ts &&...ts)
Definition: strCat.hh:591
void save(Archive &ar, const DiskChanger &changer)
Definition: DiskChanger.cc:390
Serialize (local) constructor arguments.