2

Preface: I'm very new to coding and mostly self-taught.

I'm writing a little app that will automatically copy my game save files to my backup directory. It uses the FileSystemWatcher class to watch a game's save directory and look for changes while I'm playing and calls OnChanged if a file is resized or written to.

The problem I'm having is that when my program is copying a file, it sometimes crashes the GAME (in this case Terraria). I'm guessing this has something to do with access restrictions, which I don't have much experience with. I'm surprised that File.Copy() would cause issues, since it's just reading, but it's a consistent crash with Terraria.

Stepping through the OnChanged calls has shown that Terraria writes temporary files, then copies them to the actual save files, and delete the temps. Pretty common.

So here's my code with the klutzy workaround I've got. It uses two timers: _delayTimer that starts when OnChanged is first called, and _canBackupTimer when _delayTimer elapses. I haven't had any issues in the games I've tested it with.

For each Timer, the interval is 5 seconds and AutoReset = True, so it will stop after elapsing once.

Is this the only way I can avoid IO Exceptions with the game being monitored? There has to be a better way, but I'm not sure where to look. I'm surprised that File.Copy would be restricting access to the game's save process.

Should I look at file access rights?

    private static void _delayTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e) {
        _canBackupTimer.Enabled = true;
        _canBackupTimer.Start();
        _delayTimer.Enabled = false;
    }

    private static void _canBackupTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e) {
        _lastAutoBackupTime = DateTime.Now;
        _canBackupTimer.Enabled = false;
    }


    private static void OnChanged(object source, FileSystemEventArgs e) {
        while (true) {

            if (!_canBackupTimer.Enabled && !_delayTimer.Enabled && (DateTime.Now - _lastAutoBackupTime).Seconds > 10) {
                //If neither timer is running and 10 seconds 
                //have elapsed since canBackupTimer stopped

                _delayTimer.Enabled = true;
                _delayTimer.Start();
                continue;
            }
            if (_canBackupTimer.Enabled) {
                //if canBackupTimer is running, do autobackup

                Game autoBackupGame = null;

                //_gamesToAutoBackup is a List<Game> for the program to watch.
                //Check to identify which Game is being backed up
                //g.RootFolder is the game's base directory, e.g. "...\Terraria\"
                foreach (var g in _gamesToAutoBackup) { 
                    if (e.FullPath.Contains(g.Name) || e.FullPath.Contains(g.RootFolder))
                        autoBackupGame = g;
                }

                if (autoBackupGame.RootFolder == null) {
                    var dir = new DirectoryInfo(autoBackupGame.Path);
                    autoBackupGame.RootFolder = dir.Name;
                }

                //Find the base directory of the file being changed and trim off
                //the unneeded pieces, e.g. "C:\Users\Rob\..."
                var indexOfGamePart = e.FullPath.IndexOf(autoBackupGame.RootFolder);
                var friendlyPath = e.FullPath.Substring(0, indexOfGamePart);
                var newPath = e.FullPath.Replace(friendlyPath, "\\");


                if (Directory.Exists(e.FullPath) && autoBackupGame != null) {
                    //True if directory, else it's a file.
                    //Do stuff for backing up a directory here.
                    //Currently nothing written here.
                }
                else { //If Directory.Exists is false, the path is a file.

                    try {
                        var copyDestinationFullPath = new FileInfo(_specifiedAutoBackupFolder + newPath);
                        if (!Directory.Exists(copyDestinationFullPath.DirectoryName))
                            Directory.CreateDirectory(copyDestinationFullPath.DirectoryName);
                        File.Copy(e.FullPath, copyDestinationFullPath.ToString(), true);
                    }
                    catch (FileNotFoundException ex) {
                        Logger.Log(ex); //My class, Logger, writes the exception text to a file.
                    }
                }
            }
            break;
        }
    }
CubemonkeyNYC
  • 273
  • 3
  • 17
  • 2
    What exactly does the exception say? – Lasse V. Karlsen Feb 12 '14 at 18:08
  • File.Copy does not just read the file. It reads and writes it. The exception is most likely caused because of the short interval. You may be trying to copy the temp file when the game is trying to delete the file. Or you may be trying to copy the game file when the game is trying to overwrite that specific file. The error is then thrown because the game and the file copy are colliding with each other. – deathismyfriend Feb 12 '14 at 18:18
  • @Lasse, I was able to use visual studio to debug Terraria and the error is (paraphrased) "Cannot access the file because it is in use by another program." – CubemonkeyNYC Feb 12 '14 at 18:20
  • Well, if the file is in use in such a way that you can't open it, what is the question? – Lasse V. Karlsen Feb 12 '14 at 18:22
  • @deathismyfriend, so File.Copy() reads the source and AND writes to it? That's unexpected. Do you know of a way to avoid that collision? – CubemonkeyNYC Feb 12 '14 at 18:22
  • file.copy reads from one file and writes to another, but it does need read access to the source file. If Terraria has locked that file, you can't open it while it is still in use by Terraria. – Lasse V. Karlsen Feb 12 '14 at 18:23
  • @LasseV.Karlsen - My program is accessing the save file, the game can't. So my program is making the game's save process fail. Aside from using the delay I currently have, can you suggest a way to keep my app out of the game's way, so to speak? – CubemonkeyNYC Feb 12 '14 at 18:24
  • In the case of this particular game, it seems to write and rewrite the save files for some reason. It almost seems like it saves twice. So while the game is writing/saving the second time, without the timers my program's OnChanged is copying the save files at that same time, so they collide. – CubemonkeyNYC Feb 12 '14 at 18:29
  • I'm not really sure if there is a way without knowing when the game is trying to save to the file and waiting till the save is over. Then try to copy it. Edit: try checking this out it looks like a good answer to the question. http://stackoverflow.com/questions/1406808/wait-for-file-to-be-freed-by-process/1406853#1406853 – deathismyfriend Feb 12 '14 at 18:30
  • @deathismyfriend: That won't help him. If he's copying, and then terraria comes in and tries to save over it, delete it, whatever, he'll have exactly the same problem. It's not a problem of his program failing, it's a problem of his program causing Terraria to fail. – Colin DeClue Feb 12 '14 at 18:57
  • @ColinDeClue that is true I misread when he said his program was getting the error. I thought it was his program that was erroring. – deathismyfriend Feb 13 '14 at 02:49

1 Answers1

2

My post takes the comments about Terraria into account. I haven't tried this, but here's how I would approach the problem if I wanted the best chance of copying the file while keeping Terraria from crashing.

Problem

My guess is that Terraria is crashing because it needs write access to the file while you are trying to copy it. I also thereby assume that File.Copy opens the file with no sharing access, so a different mechanism for opening and copying the file is needed to solve the problem.

Solution

You need some way to open and read from the file, while still leaving Terraria the right to simultaneously open and write to the file. I'm not 100% positive that this approach will do that, and it will depend on how Terraria is trying to open the file, but you can at least open the file so that sharing is still allowed. Instead of File.Copy, try:

using (FileStream inStream = new FileStream(filename, FileMode.Open, 
       FileAccess.Read, FileShare.ReadWrite) {
    // Specifying shared access as FileShare.ReadWrite is the key, as it lets 
    // Terraria open the file with write access.
    // Then add code here to copy the file out to your destination... 
    // this depends a bit on which version of .Net you are using, but 
    // inStream.CopyTo(outStream) is probably the easiest as long as you 
    // are using .Net 4+.
}

Conclusion

If your open fails, or your copy fails (exception), or your FileSystemWatcher signals while you are copying, it means Terraria is using the file and you will need to retry later. In any case, hopefully you will be able to stay out of Terraria's way.

I am very interested to hear if this approach works for you. Please comment if you give it a try.

Steven Hansen
  • 3,189
  • 2
  • 16
  • 12
  • Worked exactly how I needed it to. It seems like File.Copy() doesn't share access to a file, where your method does. In the event of a collision, which is still unlikely with my delay timers, the app throws a catchable IOException instead of crashing the game. The copy still works. Thanks very much for the idea. This is what I knew I needed to work with (access/sharing), but I'm very inexperienced. – CubemonkeyNYC Feb 13 '14 at 23:12